diff --git a/AGENTS_INSTALL.md b/AGENTS_INSTALL.md
index 0bab328853d..c4a80069fd7 100644
--- a/AGENTS_INSTALL.md
+++ b/AGENTS_INSTALL.md
@@ -92,7 +92,7 @@ A host that has both a Task Scheduler entry and a WinSW service is in a conflict
### The CLI
-`npm install -g --allow-scripts=bun @bitkyc08/opencodex` puts `ocx` on the PATH from npm's global prefix, and the
+`npm install -g @bitkyc08/opencodex` puts `ocx` on the PATH from npm's global prefix, and the
Bun runtime it needs is bundled inside that package. There is no separate runtime to install and
no WSL layer on Windows.
diff --git a/README.md b/README.md
index 263079fb129..db98afcaf0a 100644
--- a/README.md
+++ b/README.md
@@ -16,7 +16,7 @@ and connect OpenCode, Pi, Qoder, WorkBuddy, Kilo, Cline, T3 Code and more throug
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -94,7 +94,7 @@ providers with Codex, Claude Code, Claude Desktop, Grok Build and the tools list
### Personal install (CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+; the Bun runtime is bundled automatically
+npm install -g @bitkyc08/opencodex # Node 18+; the Bun runtime is bundled automatically
ocx start # proxy + dashboard on localhost:10100
```
@@ -253,7 +253,7 @@ they reach the npm package.
For agents
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # or `ocx service`
ocx init # interactive setup: writes ~/.opencodex/config.json and wires Codex
```
diff --git a/bin/ocx.mjs b/bin/ocx.mjs
index ef5b11ad33d..72eff87dfcf 100755
--- a/bin/ocx.mjs
+++ b/bin/ocx.mjs
@@ -331,7 +331,7 @@ async function runPackageManagerSelfUpdate(manager, child) {
const latestInvocation = managerInvocation(["view", `${PKG}@${selector}`, "version"]);
const installArgs = manager === "pnpm"
? ["add", "-g", "--allow-build=bun", `${PKG}@${selector}`]
- : ["install", "-g", "--allow-scripts=bun", `${PKG}@${selector}`];
+ : ["install", "-g", `${PKG}@${selector}`];
const installInvocation = managerInvocation(installArgs);
if (!latestInvocation || !installInvocation) {
console.error(`opencodex: could not resolve ${manager} from a trusted absolute PATH entry; aborting before stopping the proxy.`);
diff --git a/docs-site/src/components/Landing.astro b/docs-site/src/components/Landing.astro
index c7f68222199..99a3f92abb8 100644
--- a/docs-site/src/components/Landing.astro
+++ b/docs-site/src/components/Landing.astro
@@ -165,14 +165,14 @@ const providers: { label: string; path?: string }[] = [
];
const quickstartMini = [
- { cmd: 'npm install -g --allow-scripts=bun @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
+ { cmd: 'npm install -g @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
{ cmd: 'ocx init', note: t('Add a provider and connect Codex', '프로바이더 추가 후 Codex 연결', '添加提供商并连接 Codex', 'Добавьте провайдера и подключите Codex', 'プロバイダーを追加して Codex を接続', '新增供應商並連接 Codex', 'Sağlayıcı ekleyip Codex’i bağlayın') },
{ cmd: 'ocx start', note: t('Start the proxy', '프록시 시작', '启动代理', 'Запуск прокси', 'プロキシを起動', '啟動代理', "Proxy'yi başlatın") },
{ cmd: 'ocx claude', note: t('In another terminal: launch Claude Code through ocx', '다른 터미널에서: ocx를 거쳐 Claude Code 실행', '在另一个终端中:通过 ocx 启动 Claude Code', 'В другом терминале: запуск Claude Code через ocx', '別のターミナルで: ocx 経由で Claude Code を起動', '在另一個終端機中:透過 ocx 啟動 Claude Code', 'Başka bir terminalde: Claude Code’u ocx üzerinden başlatın') },
];
const quickstart = [
- { cmd: 'npm install -g --allow-scripts=bun @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
+ { cmd: 'npm install -g @bitkyc08/opencodex', note: t('Install', '설치', '安装', 'Установка', 'インストール', '安裝', 'Kurulum') },
{ cmd: 'ocx init', note: t('Add a provider and connect Codex', '프로바이더 추가 후 Codex 연결', '添加提供商并连接 Codex', 'Добавьте провайдера и подключите Codex', 'プロバイダーを追加して Codex を接続', '新增供應商並連接 Codex', 'Sağlayıcı ekleyip Codex’i bağlayın') },
{ cmd: 'ocx start', note: t('Start the proxy', '프록시 시작', '启动代理', 'Запуск прокси', 'プロキシを起動', '啟動代理', "Proxy'yi başlatın") },
{ cmd: 'codex "Write a hello world in Rust"', note: t('Codex now routes through ocx', 'Codex가 ocx를 경유', 'Codex 现在经由 ocx 路由', 'Codex теперь работает через ocx', 'Codex は ocx 経由に', 'Codex 現在經由 ocx 路由', 'Codex artık ocx üzerinden yönlendirilir') },
@@ -344,7 +344,7 @@ const docsMap = [
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex est une passerelle locale légère entre vos outils de programmation e
### Installation personnelle (CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+ ; le runtime Bun est inclus automatiquement
+npm install -g @bitkyc08/opencodex # Node 18+ ; le runtime Bun est inclus automatiquement
ocx start # proxy + tableau de bord sur localhost:10100
```
@@ -250,7 +250,7 @@ leur arrivée dans le paquet npm.
Pour les agents
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # ou `ocx service`
ocx init # configuration interactive : écrit ~/.opencodex/config.json et relie Codex
```
diff --git a/readme/README.ja.md b/readme/README.ja.md
index ba4b91408b5..734f849f17d 100644
--- a/readme/README.ja.md
+++ b/readme/README.ja.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex は、コーディングツールとモデルプロバイダーをつ
### 個人向けインストール(CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18 以上。Bun ランタイムは自動で同梱されます
+npm install -g @bitkyc08/opencodex # Node 18 以上。Bun ランタイムは自動で同梱されます
ocx start # プロキシとダッシュボードが localhost:10100 で起動
```
@@ -246,7 +246,7 @@ bun run src/cli/index.ts start
エージェント向け
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # または `ocx service`
ocx init # 対話式セットアップ: ~/.opencodex/config.json を書き、Codex を接続します
```
diff --git a/readme/README.ko.md b/readme/README.ko.md
index b631de84ad8..6586139d130 100644
--- a/readme/README.ko.md
+++ b/readme/README.ko.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex는 코딩 도구와 모델 프로바이더를 연결하는 가벼운
### 개인 설치 (CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+; Bun 런타임은 자동으로 번들됩니다
+npm install -g @bitkyc08/opencodex # Node 18+; Bun 런타임은 자동으로 번들됩니다
ocx start # 프록시 + 대시보드: localhost:10100
```
@@ -241,7 +241,7 @@ bun run src/cli/index.ts start
에이전트용
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # 또는 `ocx service`
ocx init # 대화형 설정: ~/.opencodex/config.json을 쓰고 Codex를 연결합니다
```
diff --git a/readme/README.ru.md b/readme/README.ru.md
index c1ac691fe02..0b1d8184243 100644
--- a/readme/README.ru.md
+++ b/readme/README.ru.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex — лёгкий локальный шлюз между вашими
### Личная установка (CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+; рантайм Bun подключается автоматически
+npm install -g @bitkyc08/opencodex # Node 18+; рантайм Bun подключается автоматически
ocx start # прокси + панель управления на localhost:10100
```
@@ -252,7 +252,7 @@ bun run src/cli/index.ts start
Для агентов
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # или `ocx service`
ocx init # интерактивная настройка: пишет ~/.opencodex/config.json и подключает Codex
```
diff --git a/readme/README.tr.md b/readme/README.tr.md
index 216c2d0f570..8bb58c01bcd 100644
--- a/readme/README.tr.md
+++ b/readme/README.tr.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex, kodlama araçlarınızla model sağlayıcılarınız arasında çalı
### Kişisel kurulum (CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+; Bun çalışma zamanı otomatik olarak paketlenir
+npm install -g @bitkyc08/opencodex # Node 18+; Bun çalışma zamanı otomatik olarak paketlenir
ocx start # proxy + kontrol paneli, localhost:10100
```
@@ -248,7 +248,7 @@ iyileştirmeleri ve henüz yayımlanmamış düzeltmeler npm paketine ulaşmadan
Ajanlar için
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # ya da `ocx service`
ocx init # etkileşimli kurulum: ~/.opencodex/config.json yazar ve Codex'i bağlar
```
diff --git a/readme/README.zh-CN.md b/readme/README.zh-CN.md
index 13d4c9317a1..1c0a6ef79b6 100644
--- a/readme/README.zh-CN.md
+++ b/readme/README.zh-CN.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex 是连接编程工具与模型提供商的轻量级本地网关。它
### 个人安装(CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+;Bun 运行时会自动捆绑
+npm install -g @bitkyc08/opencodex # Node 18+;Bun 运行时会自动捆绑
ocx start # 代理 + 仪表板:localhost:10100
```
@@ -241,7 +241,7 @@ bun run src/cli/index.ts start
面向 agent
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # 或 `ocx service`
ocx init # 交互式设置:写入 ~/.opencodex/config.json 并接入 Codex
```
diff --git a/readme/README.zh-TW.md b/readme/README.zh-TW.md
index 7843c589bf6..b48d492e0d5 100644
--- a/readme/README.zh-TW.md
+++ b/readme/README.zh-TW.md
@@ -15,7 +15,7 @@
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start
```
@@ -88,7 +88,7 @@ opencodex 是連接程式開發工具與模型供應商的輕量級本機閘道
### 個人安裝(CLI)
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex # Node 18+;Bun 執行環境會自動打包
+npm install -g @bitkyc08/opencodex # Node 18+;Bun 執行環境會自動打包
ocx start # 代理 + 儀表板位於 localhost:10100
```
@@ -241,7 +241,7 @@ bun run src/cli/index.ts start
給 agent 使用
```bash
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
ocx start # 或 `ocx service`
ocx init # 互動式設定:寫入 ~/.opencodex/config.json 並接上 Codex
```
diff --git a/readme/i18n-manifest.json b/readme/i18n-manifest.json
index cb937aa02f5..7fc9221d996 100644
--- a/readme/i18n-manifest.json
+++ b/readme/i18n-manifest.json
@@ -6,43 +6,43 @@
"file": "readme/README.fr.md",
"label": "Français",
"docsPath": "fr",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"ko": {
"file": "readme/README.ko.md",
"label": "한국어",
"docsPath": "ko",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"zh-CN": {
"file": "readme/README.zh-CN.md",
"label": "简体中文",
"docsPath": "zh-cn",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"zh-TW": {
"file": "readme/README.zh-TW.md",
"label": "繁體中文",
"docsPath": "zh-tw",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"ru": {
"file": "readme/README.ru.md",
"label": "Русский",
"docsPath": "ru",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"ja": {
"file": "readme/README.ja.md",
"label": "日本語",
"docsPath": "ja",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
},
"tr": {
"file": "readme/README.tr.md",
"label": "Türkçe",
"docsPath": "tr",
- "sourceSha256": "941205d861dcfa525b40c8a70ed89ede49b9591d97b30e695aaff066f70f379d"
+ "sourceSha256": "317fdcc0b4e903303817e820afd6fd620dccbcee9ab0caf9e64fd87e9a966682"
}
}
}
diff --git a/scripts/install.ps1 b/scripts/install.ps1
index 0b3ab9c3fd5..1da608a5b99 100644
--- a/scripts/install.ps1
+++ b/scripts/install.ps1
@@ -22,15 +22,15 @@ if (-not (Get-Command npm -ErrorAction SilentlyContinue)) {
Write-Host "Using Node v$nodeVersion"
-# Install opencodex globally. npm 12 blocks dependency lifecycle scripts by default, and
-# bun's postinstall is what unpacks the bundled runtime; without this approval the install
-# succeeds but `ocx` cannot start (#8361). npm 8 through 11 accept the flag on global installs.
+# Install opencodex globally
+# If npm reports "install scripts blocked" for bun, rerun as:
+# npm install -g --allow-scripts=bun @bitkyc08/opencodex
# (use an elevated PowerShell if the original install was elevated)
$npm = Get-Command npm.cmd -ErrorAction SilentlyContinue
if (-not $npm) {
$npm = Get-Command npm -ErrorAction Stop
}
-& $npm.Source install -g --allow-scripts=bun @bitkyc08/opencodex
+& $npm.Source install -g @bitkyc08/opencodex
if ($LASTEXITCODE -ne 0) {
Write-Error "npm install failed with exit code $LASTEXITCODE"
exit $LASTEXITCODE
diff --git a/scripts/install.sh b/scripts/install.sh
index 54715adc9c7..6a25db452e9 100755
--- a/scripts/install.sh
+++ b/scripts/install.sh
@@ -21,11 +21,11 @@ fi
echo "Using Node $(node --version)"
-# Install opencodex globally. npm 12 blocks dependency lifecycle scripts by default, and
-# bun's postinstall is what unpacks the bundled runtime; without this approval the install
-# succeeds but `ocx` cannot start (#8361). npm 8 through 11 accept the flag on global installs.
+# Install opencodex globally
+# If npm reports "install scripts blocked" for bun, rerun as:
+# npm install -g --allow-scripts=bun @bitkyc08/opencodex
# (keep sudo if the original install used sudo)
-npm install -g --allow-scripts=bun @bitkyc08/opencodex
+npm install -g @bitkyc08/opencodex
if ! command -v ocx &>/dev/null; then
NPM_BIN="$(npm bin -g 2>/dev/null || printf "%s/bin" "$(npm prefix -g)")"
diff --git a/scripts/test-layout/layout-route-intent.json b/scripts/test-layout/layout-route-intent.json
deleted file mode 100644
index 52cc2101adb..00000000000
--- a/scripts/test-layout/layout-route-intent.json
+++ /dev/null
@@ -1,60 +0,0 @@
-{
- "cli-link-route-intent.test.ts": "cli",
- "cli-route-to-aliases.test.ts": "cli",
- "cli-route-to-confirmation.test.ts": "cli",
- "cli-route-to.test.ts": "cli",
- "client-disconnect-config-recovery.test.ts": "clients",
- "client-disconnect-edited-codex.test.ts": "clients",
- "client-disconnect-field-snapshot.test.ts": "clients",
- "codex-disconnect-fields.test.ts": "codex-integration",
- "codex-disconnect-owned-recovery.test.ts": "codex-integration",
- "codex-journal-provider-reserialization.test.ts": "codex-integration",
- "home-route-intent-upgrade.test.ts": "clients",
- "link-home-route-intent.test.ts": "clients",
- "link-route-intent.test.ts": "server",
- "management-route-intent-native-errors.test.ts": "server",
- "management-route-intent-native-review.test.ts": "server",
- "route-intent-aside.test.ts": "clients",
- "route-intent-claude-surface.test.ts": "clients",
- "route-intent-codex-connect-disabled.test.ts": "clients",
- "route-intent-codex-transitions.test.ts": "clients",
- "route-intent-connect-recovery.test.ts": "clients",
- "route-intent-connected-review.test.ts": "clients",
- "route-intent-connected.test.ts": "clients",
- "route-intent-desktop.test.ts": "clients",
- "route-intent-file-originals.test.ts": "clients",
- "route-intent-home-consent.test.ts": "clients",
- "route-intent-local-receipt.test.ts": "clients",
- "route-intent-materializer.test.ts": "clients",
- "route-intent-native-originals.test.ts": "clients",
- "route-intent-native-review.test.ts": "clients",
- "route-intent-offline-mode-switch.test.ts": "clients",
- "route-intent-overwrite.test.ts": "clients",
- "route-intent-parity-fallback.test.ts": "clients",
- "route-intent-parity-withdrawal.test.ts": "clients",
- "route-intent-parity.test.ts": "clients",
- "route-intent-reporter.test.ts": "clients",
- "route-intent-runtime-stop.test.ts": "clients",
- "route-intent-scheduler.test.ts": "clients",
- "route-intent-state.test.ts": "clients",
- "route-intent-watch-boundary.test.ts": "server",
- "route-intent-watch-client.test.ts": "clients",
- "route-intent-watch-waiters.test.ts": "server",
- "route-intent.test.ts": "clients",
- "route-to-child-defaults.test.ts": "routing",
- "route-to-home-live-flags.test.ts": "routing",
- "link-pinned-sync-overwrite.test.ts": "clients",
- "route-to-ephemeral.test.ts": "server",
- "route-to-home-decisions.test.ts": "routing",
- "cli-sync-fanout-ephemeral.test.ts": "cli",
- "route-to-home-native-truth.test.ts": "routing",
- "route-to-home-off-review.test.ts": "routing",
- "route-to-live-settings.test.ts": "server",
- "route-to-policy-review.test.ts": "server",
- "route-to-retirement-review.test.ts": "server",
- "route-to-routes.test.ts": "server",
- "route-to-sync-child.test.ts": "routing",
- "service-disconnected-client-restart.test.ts": "service",
- "service-restart-registration-guard.test.ts": "service",
- "service-scheduler-restart-cleanup.test.ts": "service"
-}
diff --git a/scripts/test-layout/layout.json b/scripts/test-layout/layout.json
index 1ca32d3d7ec..aed2989c321 100644
--- a/scripts/test-layout/layout.json
+++ b/scripts/test-layout/layout.json
@@ -1,7 +1,7 @@
{
"version": 1,
"root": "tests",
- "explicitShards": ["layout-route-intent.json", "layout-file-identity.json", "layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-server-inbound.json", "layout-providers.json", "layout-responses.json", "layout-exec-boundary.json", "layout-persistent-dashboard.json", "layout-start-ownership.json", "layout-config.json", "layout-kiro-usage.json", "layout-usage.json"],
+ "explicitShards": ["layout-file-identity.json", "layout-clients.json", "layout-codex-collision.json", "layout-agent-assertion.json", "layout-chat.json", "layout-replay.json", "layout-adapters.json", "layout-jev.json", "layout-workbuddy.json", "layout-combos.json", "layout-credit-renewal.json", "layout-server-inbound.json", "layout-providers.json", "layout-responses.json", "layout-exec-boundary.json", "layout-persistent-dashboard.json", "layout-start-ownership.json", "layout-config.json", "layout-kiro-usage.json", "layout-usage.json"],
"explicit": { "kimi-monthly-quota.test.ts": "providers", "sync-fanout.test.ts": "cli", "sync-command-fanout.test.ts": "cli", "link-pinned-sync-argv.test.ts": "clients", "sync-link-status.test.ts": "cli", "sync-fanout-process.test.ts": "cli", "codex-installation-acl.test.ts": "codex-integration", "codex-installation-id.test.ts": "codex-integration", "codex-installation-wire.test.ts": "codex-integration", "codex-installation-failover.test.ts": "responses", "service-desktop-recovery.test.ts": "service", "catalog-omit-disabled-native.test.ts": "codex-integration", "provider-input-modalities.test.ts": "providers", "combo-description.test.ts": "routing", "combo-description-catalog.test.ts": "codex-integration", "codex-provider-routing.test.ts": "codex-integration", "config-native-reasoning-retention.test.ts": "config", "server-native-reasoning-retention-merge.test.ts": "server", "server-native-reasoning-retention.test.ts": "server", "config-workflow-budget.test.ts": "config", "server-workflow-budget-settings.test.ts": "server", "responses-retained-reasoning-stream-recovery.test.ts": "responses", "openai-native-hosted-search-history.test.ts": "adapters/openai", "responses-native-compact-search-history.test.ts": "responses", "config-hosted-search-projection.test.ts": "config", "gcp-credential-boundary.test.ts": "adapters/google", "gcp-sa-marker.test.ts": "adapters/google", "gcp-sa-adapter.test.ts": "adapters/google", "management-gcp-credential-input.test.ts": "server", "codex-preserve-signed-in.test.ts": "codex-integration", "cli-account-preservation.test.ts": "cli", "cli-provider-list-active.test.ts": "cli", "service-state-isolation.test.ts": "service", "compaction-history-framing.test.ts": "responses", "catalog-native-max-context.test.ts": "codex-integration", "protocol-argon-thought-boundary.test.ts": "responses", "service-log.test.ts": "service", "messages-native-empty-inline.test.ts": "claude-integration", "catalog-auto-review-override.test.ts": "codex-integration", "management-auto-review-settings.test.ts": "server", "management-auto-review-auth.test.ts": "server", "prefer-model-alias-ids.test.ts": "server", "responses-mimo-envelope.test.ts": "responses", "responses-call-id-linkage.test.ts": "responses", "bridge-terminal-refusal.test.ts": "adapters", "loop-terminal-refusal.test.ts": "images", "web-search-terminal-refusal.test.ts": "web-search", "combo-terminal-refusal.test.ts": "routing", "server-terminal-refusal-wiring.test.ts": "server", "incomplete-reason-normalization.test.ts": "responses", "advisor-retention.test.ts": "advisor", "responses-native-key-cooldown.test.ts": "responses", "responses-run-turn-key-readmission.test.ts": "responses", "qoder-spawn-readmission.test.ts": "adapters", "api-key-selection-cooldown.test.ts": "providers", "api-key-auto-pause-distribution.test.ts": "providers", "explicit-key-pause-oauth-boundary.test.ts": "providers", "server-chat-native-key-selection.test.ts": "server", "compact-key-selection.test.ts": "responses", "messages-native-key-selection.test.ts": "claude-integration", "coding-agent-windows-dispatch.test.ts": "adapters", "coding-agent-native-empty-argv.test.ts": "windows",
"stop-restore-history-ownership.test.ts": "service",
"cli-stop-refusal-transport.test.ts": "cli",
@@ -202,7 +202,7 @@
"anthropic-empty-content.test.ts": "adapters/anthropic",
"anthropic-eof-tolerance.test.ts": "adapters/anthropic",
"anthropic-error-body.test.ts": "adapters/anthropic",
- "anthropic-error-stop-reason.test.ts": "adapters/anthropic", "anthropic-thinking-usage.test.ts": "adapters/anthropic", "anthropic-thinking-display-policy.test.ts": "adapters/anthropic",
+ "anthropic-error-stop-reason.test.ts": "adapters/anthropic", "anthropic-thinking-usage.test.ts": "adapters/anthropic",
"anthropic-fast-opt-in.test.ts": "adapters/anthropic",
"anthropic-fast-speed.test.ts": "adapters/anthropic",
"anthropic-hardening.test.ts": "adapters/anthropic",
@@ -777,7 +777,7 @@
"config-non-object-backup.test.ts": "config",
"config-ownership-uninstall.test.ts": "config", "config-ownership-concurrent-record.test.ts": "config", "config-ownership-removal.test.ts": "config",
"config-rebase-provenance-writers.test.ts": "config",
- "config-save-boundary.test.ts": "config", "config-thinking-display.test.ts": "config",
+ "config-save-boundary.test.ts": "config",
"config-spend-ceilings.test.ts": "config",
"config-spend-pool-collision.test.ts": "config",
"config-user-edits.test.ts": "config", "hub-disable-live-save.test.ts": "config",
@@ -925,7 +925,7 @@
"docs-readme-memory-inventory.test.ts": "ci-workflows",
"docs-readme-translation-parity.test.ts": "ci-workflows",
"docs-remote-hub-claims.test.ts": "ci-workflows",
- "doctor-codex-envkey-readiness.test.ts": "service", "doctor-bun-install.test.ts": "service",
+ "doctor-codex-envkey-readiness.test.ts": "service",
"doctor-oauth.test.ts": "service",
"doctor-provider-apikey.test.ts": "service",
"doctor-service-memory-contract.test.ts": "service",
@@ -1217,7 +1217,7 @@
"management-provider-reset-replay.test.ts": "server",
"management-provider-synthetic-max.test.ts": "server",
"management-provider-upstream-websocket.test.ts": "server",
- "management-provider-context-inference.test.ts": "server", "management-provider-validation.test.ts": "server", "management-provider-thinking-display.test.ts": "server",
+ "management-provider-context-inference.test.ts": "server", "management-provider-validation.test.ts": "server",
"management-provider-verbosity.test.ts": "server",
"management-route-registry.test.ts": "server",
"protocol-routes.test.ts": "server",
@@ -1839,7 +1839,7 @@
"update-async-routes.test.ts": "server",
"update-badge.test.ts": "update", "update-bounded-json.test.ts": "update", "update-checkpoint-codec.test.ts": "update", "update-contracts.test.ts": "update", "update-candidate-id.test.ts": "update", "update-external-metadata.test.ts": "update", "update-minisign.test.ts": "update", "update-root-snapshot.test.ts": "update", "update-signed-supply.test.ts": "update", "update-signed-version.test.ts": "update", "update-trust-root.test.ts": "update", "update-bun-ownership-lease.test.ts": "update",
"update-desktop-badge.test.ts": "update", "update-desktop-owner.test.ts": "update", "update-supervised-recovery.test.ts": "update",
- "update-job.test.ts": "update", "update-recovery-host-free.test.ts": "update", "update-job-supervised-restart.test.ts": "update", "update-standalone.test.ts": "update", "update-worker-launch.test.ts": "update", "update-npm-allow-scripts.test.ts": "update",
+ "update-job.test.ts": "update", "update-recovery-host-free.test.ts": "update", "update-job-supervised-restart.test.ts": "update", "update-standalone.test.ts": "update", "update-worker-launch.test.ts": "update",
"update-notify.test.ts": "update", "update-npm-cache-preflight.test.ts": "update",
"update-orchestrated-session.test.ts": "update", "update-orchestrated-entry.test.ts": "update", "update-orchestrated-apply.test.ts": "update", "update-orchestrated-arguments.test.ts": "update", "update-orchestrated-plan.test.ts": "update", "update-sync-mutex.test.ts": "update", "update-orchestrated-identity.test.ts": "update", "update-orchestrated-token.test.ts": "update", "update-npm-invocation.test.ts": "update", "update-npm-timeout.test.ts": "update", "update-npm-supervisor-exit.test.ts": "update", "update-mise-launcher-target.test.ts": "update",
"update-mise.test.ts": "update", "update-mise-node-runtime.test.ts": "update",
diff --git a/skills/ocx/references/01_management_surface.md b/skills/ocx/references/01_management_surface.md
index f01318a08f4..d49db6cfa0b 100644
--- a/skills/ocx/references/01_management_surface.md
+++ b/skills/ocx/references/01_management_surface.md
@@ -28,7 +28,7 @@ These answer in the CLI head and never reach the proxy, so they work with nothin
| [lifecycle](01_surface_lifecycle.md) | 14 |
| [providers-models](01_surface_providers-models.md) | 50 |
| [accounts](01_surface_accounts.md) | 43 |
-| [agents-routing](01_surface_agents-routing.md) | 71 |
+| [agents-routing](01_surface_agents-routing.md) | 68 |
| [integrations](01_surface_integrations.md) | 41 |
| [observe-system](01_surface_observe-system.md) | 99 |
| [access-remote](01_surface_access-remote.md) | 34 |
@@ -1527,20 +1527,8 @@ Original invocation order. These headings preserve links to the previous single-
[State-changing task](01_surface_lab.md#ocx-lab-run)
-### `ocx route to`
-
-[State-changing task](01_surface_agents-routing.md#ocx-route-to)
-
-### `ocx route to status`
-
-[Read-oriented task](01_surface_agents-routing.md#ocx-route-to-status)
-
-### `ocx route to sync`
-
-[State-changing task](01_surface_agents-routing.md#ocx-route-to-sync)
-
## Counts
-- declared capabilities: 373
-- of those, state-changing: 230
+- declared capabilities: 370
+- of those, state-changing: 228
- head-resolved invocations: 2
diff --git a/skills/ocx/references/01_surface_access-remote.md b/skills/ocx/references/01_surface_access-remote.md
index bdcc876c627..3a9df271275 100644
--- a/skills/ocx/references/01_surface_access-remote.md
+++ b/skills/ocx/references/01_surface_access-remote.md
@@ -81,7 +81,6 @@ State-changing: yes. JSON mode: `payload`.
| Flag | Value | Meaning |
|---|---|---|
-| `--clients` | string | Initial device selection; defaults to Codex and Claude Code. Other Home client defaults remain inherited. |
| `--alias` | string | SSH host alias for the linked machine. |
| `--tunnel-port` | number | Remote loopback port for the reverse tunnel. |
| `--json` | boolean | Emit the issue result as JSON. |
@@ -329,7 +328,7 @@ State-changing: yes. JSON mode: `payload`.
### `ocx disconnect`
-Usage: `ocx disconnect [--keep-catalog] [--recover-config --yes] [--json] | ocx disconnect --orphaned-token --yes [--json]`
+Usage: `ocx disconnect [--keep-catalog] [--json] | ocx disconnect --orphaned-token --yes [--json]`
Disconnect a client or preserve orphaned token files for recovery.
@@ -340,9 +339,8 @@ Drives no management route.
| Flag | Value | Meaning |
|---|---|---|
| `--keep-catalog` | boolean | Keep the connected client's catalog during ordinary disconnect. |
-| `--recover-config` | boolean | With --yes, snapshot current Codex artifacts and recover only evidenced owned keys; unknown ownership still refuses. |
| `--orphaned-token` | boolean | Preserve unowned service-token files in a private backup when disconnected. |
-| `--yes` | boolean | Explicitly confirm --recover-config or --orphaned-token recovery. |
+| `--yes` | boolean | Explicitly confirm orphan-token recovery; requires --orphaned-token. |
| `--json` | boolean | Emit the local result and backup location as JSON. |
- Orphan recovery requires a disconnected durable state, no pending connect or disconnect, no Hub or admission-secret use, no installed or unverified service, and verified absence of a client runtime. It preserves rather than deletes token files and does not establish their ownership.
diff --git a/skills/ocx/references/01_surface_agents-routing.md b/skills/ocx/references/01_surface_agents-routing.md
index 10187ebd5ec..b30b7d6f50c 100644
--- a/skills/ocx/references/01_surface_agents-routing.md
+++ b/skills/ocx/references/01_surface_agents-routing.md
@@ -8,7 +8,7 @@
Use these declarations to choose a task, then check its flags and authority before execution.
Non-mutating probes may still contact providers, consume quota or refresh caches.
-Declared capabilities: 71.
+Declared capabilities: 68.
### `ocx agent subagents force`
@@ -1339,69 +1339,3 @@ State-changing: no. JSON mode: `payload`.
| `--json` | boolean | Emit the result as JSON. |
- Alias of observe memory.
-
-### `ocx route to`
-
-Usage: `ocx route to [--device ] [--accounts ] [--model-names ] [--overwrite-conflict] [--local] [--json]`
-
-Set Home-default or per-device route intent, or opt out locally on a Child.
-
-State-changing: yes. JSON mode: `payload`.
-
-| Method | Route |
-|---|---|
-| PUT | `/api/route-to` |
-
-| Flag | Value | Meaning |
-|---|---|---|
-| `--device` | string | Home: alias, id, all or this. Child: this or its own link id only. |
-| `--json` | boolean | Emit the ocx.route-to/1 status matrix. |
-| `--overwrite-conflict` | boolean | Explicitly permit replacing conflicting managed blocks. |
-| `--local` | boolean | Child only: opt out with off, or clear the opt-out with inherit. |
-| `--accounts` | string | Aside on only: all, none, or comma-separated stable userIds; never u/N slots. |
-| `--model-names` | string | Hermes/Command Code on only: alias or routed model names. |
-
-- Clients: codex, claude, claude-desktop, grok and the supported file integrations. Without --device a Home updates its default and materializes itself; all is only for status/sync.
-- inherit requires --device unless clearing a Child --local opt-out. Child writes print the exact Home command with the Child link id; --local supports off/inherit only. --overwrite-conflict applies only to on or sync.
-- On the Home, codex, claude, grok and claude-desktop follow the Home's own native switches: a Home default or --device home write for them writes that switch (Home inherit sets it to the default).
-
-### `ocx route to status`
-
-Usage: `ocx route to status [--device ] [--json]`
-
-Read per-device route intent, its source and actual client apply outcomes.
-
-State-changing: no. JSON mode: `payload`.
-
-| Method | Route |
-|---|---|
-| GET | `/api/route-to` |
-
-| Flag | Value | Meaning |
-|---|---|---|
-| `--device` | string | Home: alias, id, all or this. Child: this or its own link id only. |
-| `--json` | boolean | Emit the ocx.route-to/1 status matrix. |
-
-- On a Child this reads local receipts without a Home management relay. Intent and applied state are distinct; null applied means no recorded outcome.
-- Sources: builtin, default, device, local (Child opt-out) and native (a Home row governed by the Home's own switch). An outcome recorded for an earlier intent reads stale.
-
-### `ocx route to sync`
-
-Usage: `ocx route to sync [--device ] [--overwrite-conflict] [--json]`
-
-Reapply route intent on selected devices, or locally on a Child.
-
-State-changing: yes. JSON mode: `payload`.
-
-| Method | Route |
-|---|---|
-| POST | `/api/route-to/sync` |
-
-| Flag | Value | Meaning |
-|---|---|---|
-| `--device` | string | Home: alias, id, all or this. Child: this or its own link id only. |
-| `--overwrite-conflict` | boolean | Explicitly permit replacing conflicting managed blocks. |
-| `--json` | boolean | Emit the ocx.route-to/1 status matrix. |
-
-- No conflict retry or implicit overwrite. Child sync calls its local materializer and sends no Home management write.
-- Home sync never changes the Home's native switches for codex, claude, grok or claude-desktop; it refreshes what each switch says.
diff --git a/skills/ocx/references/01_surface_integrations.md b/skills/ocx/references/01_surface_integrations.md
index f7e55100a69..067e554644d 100644
--- a/skills/ocx/references/01_surface_integrations.md
+++ b/skills/ocx/references/01_surface_integrations.md
@@ -192,7 +192,7 @@ State-changing: yes. JSON mode: `payload`.
|---|---|---|
| `--json` | boolean | Emit the client rows or toggle result as JSON. |
-- Each on/off writes the Home's native switch, recording no route intent for the Home, and applies it through the client's runtime owner, including Claude Desktop's first-party transition; Children keep inheriting their saved default. Refused disables remain failures. On a Child it is refused with the exact Home command. Cursor status and local-installer are separate read-only subcommands; they do not toggle or install Cursor.
+- Each toggle writes the selected client configuration through its runtime owner. Refused disables remain failures. Cursor status and local-installer are separate read-only subcommands; they do not toggle or install Cursor.
### `ocx integration client`
@@ -228,7 +228,7 @@ State-changing: yes. JSON mode: `payload`.
| `--confirm-drift` | boolean | Explicitly allow replacing edits made after the snapshot. |
- status/show/list reads all clients, one client or Aside profiles. history/journal reads rollback records; expired snapshots stay visible.
-- enable/disable requires --client; an omitted Aside --profile toggles all profiles. Each toggle is this Home's own decision, like the dashboard: once the Home uses route intent it is recorded so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). --overwrite-conflict applies only to enable.
+- enable/disable requires --client; an omitted Aside --profile toggles all profiles. --overwrite-conflict applies only to enable.
- restore requires --op (alias --op-id); --client requires --profile and only Aside supports that profile selector. --confirm-drift is an explicit user waiver, never auto-retried. Use client preview or restore --preview to inspect a change, history remove --op ID --yes to retire a journal row, and sync --client aside to refresh managed profiles.
### `ocx grok status`
@@ -417,7 +417,7 @@ State-changing: yes. JSON mode: `payload`.
| `--reasoning-default` | string | Repeat MODEL=EFFORT to replace the full Droid defaults map; exact model IDs, duplicate keys refused. |
| `--clear-reasoning-defaults` | boolean | Droid apply/overwrite only: send an empty map, distinct from omitted inherited defaults. |
-- A Home-only decision, like the dashboard toggle: once the Home uses route intent the server records it as this Home's override so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). On a Child a plain toggle is refused with the Home command. An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.
+- An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.
- Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.
- Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview.
- Droid map omission preserves inheritance; supplied entries replace the full map, clear sends {}. Only Droid apply/overwrite accepts these options.
@@ -443,7 +443,7 @@ State-changing: yes. JSON mode: `payload`.
| `--json` | boolean | Emit the result as JSON. |
| `--plan-fingerprint` | string | Optional versioned token from a matching preview; stale intent is refused without retry. |
-- A Home-only decision, like the dashboard toggle: once the Home uses route intent the server records it as this Home's override so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). On a Child a plain toggle is refused with the Home command. An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.
+- An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.
- Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.
- Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview.
diff --git a/skills/ocx/references/01_surface_lifecycle.md b/skills/ocx/references/01_surface_lifecycle.md
index 9d4e6edbac6..ce7372e8b50 100644
--- a/skills/ocx/references/01_surface_lifecycle.md
+++ b/skills/ocx/references/01_surface_lifecycle.md
@@ -85,7 +85,7 @@ Drives no management route.
### `ocx sync`
-Usage: `ocx sync [--expect-link | --expect-hub-key ] [--overwrite-conflict [--overwrite-client ]] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]`
+Usage: `ocx sync [--expect-link | --expect-hub-key ] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]`
Synchronize client catalogs, including Aside profiles through the running server's mutation owner.
@@ -97,8 +97,6 @@ State-changing: yes. JSON mode: `payload`.
| Flag | Value | Meaning |
|---|---|---|
-| `--overwrite-conflict` | boolean | Allow conflicting route materialization only for this explicit sync. |
-| `--overwrite-client` | string | Restrict takeover to one client; requires --overwrite-conflict. |
| `--local-only` | boolean | Do not fan out the Home sync to linked Children. |
| `--json` | boolean | Emit the sync fan-out result as JSON. |
| `--restart-codex` | boolean | Restart the Codex app-servers and fully quit and relaunch the Codex desktop app after a catalog, cache, or changed configuration write, on macOS, Linux and Windows. |
diff --git a/src/adapters/anthropic.ts b/src/adapters/anthropic.ts
index 87a4071f22d..f864bd06468 100644
--- a/src/adapters/anthropic.ts
+++ b/src/adapters/anthropic.ts
@@ -1109,18 +1109,6 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
const summary = (parsed._rawBody as { reasoning?: { summary?: unknown } } | undefined)?.reasoning?.summary;
// Codex sends "auto" by default, so only an explicit concise/detailed request opts out of updates (#7728).
const summaryRequested = summary === "concise" || summary === "detailed";
- // Operator policy for the adaptive `thinking.display` field (#8363). Unset and "auto" keep
- // the built-in selection. Any model entry wins over the provider-wide value, including an
- // explicit "auto" that opts one model back out. "updates" is only sendable where this
- // adapter already sent it (native Opus 5.5), so anywhere else it is treated as auto rather
- // than inventing a display value the model never received.
- const modelThinkingDisplay = modelRecordValue(provider.modelThinkingDisplay, parsed.modelId);
- const configuredThinkingDisplay = modelThinkingDisplay !== undefined
- ? modelThinkingDisplay
- : provider.thinkingDisplay;
- const thinkingDisplayPolicy = configuredThinkingDisplay === "updates" && !nativeOpus55
- ? "auto"
- : configuredThinkingDisplay ?? "auto";
const thinkingToggleWire = declaresThinkingToggleWire(provider, parsed.modelId);
const reasoningControlsUnsupported = reasoningControlsAreUnsupported(provider, parsed.modelId);
const capabilities = resolveReasoningCapabilities(provider, parsed.modelId);
@@ -1157,16 +1145,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
// thinking blocks, so a Chat client sees minutes of heartbeats and no reasoning delta
// during a long think (#5824). Native Opus 5.5 selects its display below;
// other models request summarized thinking unless the caller hides it.
- // Under the default policy this stays the pre-existing selection, including its local
- // hideThinkingSummary behavior; a configured "summarized" overrides that hiding.
- // An explicit request summary forces summarized on every adaptive model (#7881), so it
- // also outranks a configured "omitted". "updates" already degraded to auto above
- // because only native Opus 5.5 can send it.
- // Local hideThinkingSummary only speaks for the built-in policy, where it keeps its
- // pre-existing meaning; an operator choice replaces that local preference.
- const hiddenThinkingDisplay = (thinkingDisplayPolicy === "omitted" && !summaryRequested)
- || (thinkingDisplayPolicy === "auto" && parsed.options.hideThinkingSummary);
- body.thinking = hiddenThinkingDisplay
+ body.thinking = parsed.options.hideThinkingSummary
? { type: "adaptive" }
: { type: "adaptive", display: "summarized" };
const effort = adaptiveEffort(effectiveReasoning);
@@ -1214,12 +1193,7 @@ export function createAnthropicAdapter(provider: OcxProviderConfig, cacheRetenti
}
if (nativeOpus55 && !reasoningControlsUnsupported && thinkingEnabled !== false) {
- // An explicit per-request summary (concise/detailed) still forces summarized (#7881).
- body.thinking = summaryRequested || thinkingDisplayPolicy === "summarized"
- ? { type: "adaptive", display: "summarized" }
- : thinkingDisplayPolicy === "omitted"
- ? { type: "adaptive" }
- : { type: "adaptive", display: "updates" };
+ body.thinking = { type: "adaptive", display: summaryRequested ? "summarized" : "updates" };
}
// One discriminator governs the display beta and response classification, including reuse.
progressUpdates = (body.thinking as { display?: string } | undefined)?.display === "updates";
diff --git a/src/adapters/google.ts b/src/adapters/google.ts
index 97d6167f30a..c6ff9d971a3 100644
--- a/src/adapters/google.ts
+++ b/src/adapters/google.ts
@@ -22,7 +22,6 @@ import { contentPartsToText, parseDataUrl } from "./image";
import { bindVertexRequestAuthority, GCP_CREDENTIAL_MARKER_PREFIX, gcpCredentialMarkerAccount, getVertexAccessTokenWithAuthority } from "../lib/gcp-adc";
import { fetchAntigravityWithRetry, fetchVertexWithRetry } from "./google-http";
import { safeAntigravityHttpErrorMessage, safeVertexHttpErrorMessage } from "./google-errors";
-import { readDisplaySafeErrorPayloadText } from "./upstream-http-error";
import { isVertexTruncatedTurn, vertexTruncationErrorEvent } from "./google-truncation";
import { antigravitySessionAnchor, antigravityUserAgentForWireModel, antigravitySessionId, isLikelyRealThoughtSignature, sanitizeAntigravityClaudeSignatures } from "./google-antigravity-wire";
import { summarizeGoogleWireShape } from "./google-wire-shape";
@@ -947,15 +946,11 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
// session replay cache), but durable-store eviction is intentionally narrower: it only runs
// when the error text explicitly mentions a signature, so a generic tool-schema
// INVALID_ARGUMENT does not destroy valid durable signatures.
- //
- // `httpStatus` is the real upstream status when the rejection arrived as an HTTP error. The
- // normalized Vertex/Antigravity error text keeps only Google's message, not its status enum,
- // so a 400 such as "bad model" would otherwise leave the poisoned session entry in place (#8054).
- function handleSignatureRejection(errorMessage?: string, httpStatus?: number) {
+ function handleSignatureRejection(errorMessage?: string) {
const replayModel = provider.googleMode === "cloud-code-assist" ? antigravityModel : vertexReplayModel;
const replaySession = provider.googleMode === "cloud-code-assist" ? antigravitySession : vertexReplaySession;
const text = errorMessage ?? "";
- const isInvalidArgument = httpStatus === 400 || /invalid_argument|invalid argument/i.test(text);
+ const isInvalidArgument = /invalid_argument|invalid argument/i.test(text);
const isSignatureError = /signature|thought_signature|thoughtSignature/i.test(text);
// The in-memory Antigravity replay cache only exists for CCA/Vertex, so clearing it stays
// scoped to those modes (replayModel/replaySession are undefined elsewhere anyway).
@@ -987,23 +982,12 @@ export function createGoogleAdapter(provider: OcxProviderConfig): ProviderAdapte
// 429 rotation and raw error formatting.
...(provider.googleMode === "vertex" || provider.googleMode === "cloud-code-assist"
? {
- fetchResponse: async (request: AdapterRequest, ctx?: AdapterFetchContext): Promise => {
- const res = await (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
+ fetchResponse: (request: AdapterRequest, ctx?: AdapterFetchContext): Promise =>
+ (provider.googleMode === "cloud-code-assist" ? fetchAntigravityWithRetry : fetchVertexWithRetry)(
request,
ctx,
{ toolSchemaProfile, toolSchemaPolicy },
- );
- // A rejected replay usually arrives as a real HTTP 400, and the dispatcher reports a
- // non-ok response without ever calling parseStream/parseResponse — the only other
- // callers of clear-on-invalid. Without this, the bad signature stays cached and is
- // replayed on every turn until the TTL expires (#8054). Read a clone so the body the
- // dispatcher formats is untouched, and await it so the next turn cannot race ahead of
- // the eviction. No resend here: payload rejection stays resend-is-futile.
- if (res.status === 400) {
- handleSignatureRejection(await readDisplaySafeErrorPayloadText(res.clone(), ctx?.abortSignal), res.status);
- }
- return res;
- },
+ ),
formatErrorBody: (status: number, _headers: Headers, payloadText: string): string =>
(provider.googleMode === "cloud-code-assist" ? safeAntigravityHttpErrorMessage : safeVertexHttpErrorMessage)(status, payloadText),
}
diff --git a/src/claude/desktop-3p.ts b/src/claude/desktop-3p.ts
index 3b91d3b26c4..25d0f85e509 100644
--- a/src/claude/desktop-3p.ts
+++ b/src/claude/desktop-3p.ts
@@ -1,6 +1,6 @@
import { readClientConnectionState } from "../client/state";
import { readServiceApiTokenState, readTokenBackupState } from "../lib/service-secrets";
-import { assertClientLifecycleHeld, withClientLifecycleSync, type ClientLifecycleHeld, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
+import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
import { applyRemoteDesktopStore, inspectRemoteDesktopCleanup, restoreRemoteDesktopStore } from "./desktop-remote-store";
import { canonicalDirectory } from "./desktop-remote-store-io";
import {
@@ -571,7 +571,6 @@ export function removeDesktop3pStandardPivot(
options: Desktop3pConfigLibraryOptions & {
appliedFingerprint?: string | null; unlink?: (path: string) => void;
lifecycleLockDeps?: ClientLifecycleLockDeps;
- lifecycleHeld?: ClientLifecycleHeld;
/**
* The desired-state guard below exists for OFF flows racing a concurrent enable. A
* mode switch (gateway → first-party) removes the profile while the integration stays
@@ -582,7 +581,7 @@ export function removeDesktop3pStandardPivot(
): Desktop3pRemovalResult {
const libraryPath = resolveDesktop3pConfigLibraryPath(options);
try {
- const remove = (held: ClientLifecycleHeld): Desktop3pRemovalResult => withConfigMutationLockSync(() => {
+ return withClientLifecycleSync(held => withConfigMutationLockSync(() => {
const connection = readClientConnectionState();
if (connection.kind === "invalid" || connection.kind === "mismatched") {
return { ok: false, changed: false, kind: "unsafe", libraryPath, reason: `desktop_client_state_${connection.kind}` };
@@ -622,9 +621,7 @@ export function removeDesktop3pStandardPivot(
return result.ok
? { ok: true, changed: result.changed, kind: result.changed ? "removed" : "noop", libraryPath }
: { ok: false, changed: result.changed, kind: result.reason === "cleanup_pending" ? "cleanup_incomplete" : "unsafe", reason: result.reason, libraryPath };
- });
- if (options.lifecycleHeld) { assertClientLifecycleHeld(options.lifecycleHeld); return remove(options.lifecycleHeld); }
- return withClientLifecycleSync(remove, options.lifecycleLockDeps);
+ }), options.lifecycleLockDeps);
} catch (error) {
return { ok: false, changed: false, kind: "write_failed", libraryPath, reason: desktopMutationFailureReason(error) };
}
@@ -727,10 +724,9 @@ export function writeDesktop3pConfig(
nativeContextCap?: NativeContextLimitsInput,
lifecycleLockDeps?: ClientLifecycleLockDeps,
refreshOnly?: { appliedFingerprint: string; admit: () => boolean },
- lifecycleHeld?: ClientLifecycleHeld,
): { written: boolean; path: string; reason?: string; fingerprint?: string } {
try {
- const write = () => withConfigMutationLockSync(() => {
+ return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
const connection = readClientConnectionState();
if (connection.kind === "invalid" || connection.kind === "mismatched") {
return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: `desktop_client_state_${connection.kind}` };
@@ -789,9 +785,7 @@ export function writeDesktop3pConfig(
generateDesktop3pConfig(destination.origin, nativeSlugs, routedModels, gatewayKey, mode, profile, nativeContextCap,
desktop3pModelOptions(latest.config.claudeCode))
), requiredId && refreshOnly ? { requiredId, expectedFingerprint: refreshOnly.appliedFingerprint, admit: refreshOnly.admit } : undefined);
- });
- if (lifecycleHeld) { assertClientLifecycleHeld(lifecycleHeld); return write(); }
- return withClientLifecycleSync(write, lifecycleLockDeps);
+ }), lifecycleLockDeps);
} catch { return { written: false, path: resolveDesktop3pConfigLibraryPath(), reason: "desktop_lifecycle_busy_or_unsafe" }; }
}
diff --git a/src/claude/desktop-gateway-state.ts b/src/claude/desktop-gateway-state.ts
index d9b1d4c373b..322970c0560 100644
--- a/src/claude/desktop-gateway-state.ts
+++ b/src/claude/desktop-gateway-state.ts
@@ -25,12 +25,10 @@ export function persistCommittedDesktopGateway(
snapshot: OcxConfig,
profile: DesktopProfile | undefined,
fingerprint: string | undefined,
- admit?: (current: OcxConfig) => boolean,
): { ok: true } | { ok: false; reason: "missing" | "invalid" | "conflict" | "unavailable" } {
const appliedAt = new Date().toISOString();
try {
const outcome = mutatePersistedConfig(current => {
- if (admit && !admit(current)) return { changed: false, value: null };
if (JSON.stringify(current.claudeCode?.desktopProfile) !== JSON.stringify(profile)) {
return { changed: false, value: null };
}
diff --git a/src/claude/desktop-remote-store-artifact.ts b/src/claude/desktop-remote-store-artifact.ts
index cc79200d22a..ecefac46cc6 100644
--- a/src/claude/desktop-remote-store-artifact.ts
+++ b/src/claude/desktop-remote-store-artifact.ts
@@ -5,7 +5,7 @@ import { readServiceApiTokenState, readTokenBackupState, serviceApiTokenFingerpr
import { profilePath, type Desktop3pMetadata } from "./desktop-3p-library";
import { StoreIO, metadata, storePaths, MAX_DESKTOP_METADATA_ENTRIES, type JsonFile } from "./desktop-remote-store-io";
import {
- DesktopStoreError, canonical, digest, origin, parseBaseline, parseDisconnect, parseOwner, parseState, projection, projectionHash, mergeProjection,
+ DesktopStoreError, digest, origin, parseBaseline, parseDisconnect, parseOwner, parseState, projection, projectionHash, mergeProjection,
sameOwner, type ArtifactPending, type Baseline, type DesktopRemoteOwner, type Projection, type StoreState,
} from "./desktop-remote-store-state";
@@ -125,7 +125,7 @@ export function establish(io: StoreIO, owner: DesktopRemoteOwner, known: readonl
return bundle;
}
-export function artifact(io: StoreIO, bundle: Bundle, next: Projection, kind: ArtifactPending["kind"], tokenFingerprint: string, desiredSelection?: string | null, overwriteConflict = false): void {
+export function artifact(io: StoreIO, bundle: Bundle, next: Projection, kind: ArtifactPending["kind"], tokenFingerprint: string, desiredSelection?: string | null): void {
const state = bundle.state!;
const targetPath = profilePath(bundle.paths.library, state.targetId);
let current = io.read(targetPath);
@@ -143,18 +143,7 @@ export function artifact(io: StoreIO, bundle: Bundle, next: Projection, kind: Ar
if (pending.kind !== kind || pending.after !== after || pending.tokenFingerprint !== tokenFingerprint
|| (before !== pending.before && before !== pending.after)) throw new DesktopStoreError("conflict");
if (kind === "apply" && selection !== pending.beforeSelection && selection !== pending.afterSelection) throw new DesktopStoreError("conflict");
- } else if (before !== state.lastProjectionHash) {
- // Consent covers only edited fields of the selected, settled, already-owned row.
- // Owner, path, metadata, token and pending-transaction checks remain mandatory.
- if (!overwriteConflict || kind !== "apply" || state.phase !== "active" || !current
- || !existing || selection !== state.targetId) throw new DesktopStoreError("conflict");
- const snapshotPath = join(bundle.paths.root, "overwrites", `${current.hash}.json`);
- const snapshot = io.read(snapshotPath, 1024 * 1024, true);
- if (snapshot && canonical(snapshot.value) !== canonical(current.value)) throw new DesktopStoreError("conflict");
- if (!snapshot) io.write(snapshotPath, current.value, null);
- // artifact's ordinary pending record now journals this exact edited preimage.
- io.compare(targetPath, current);
- }
+ } else if (before !== state.lastProjectionHash) throw new DesktopStoreError("conflict");
const afterSelection = desiredSelection === undefined ? selection : desiredSelection;
const settledPhase = kind === "restore" ? "restored" : "active";
if (!pending && existing && before === after && afterSelection === selection
diff --git a/src/claude/desktop-remote-store.ts b/src/claude/desktop-remote-store.ts
index 1ae510dfbd5..3e18307c980 100644
--- a/src/claude/desktop-remote-store.ts
+++ b/src/claude/desktop-remote-store.ts
@@ -81,32 +81,24 @@ export function inspectRemoteDesktopStore(owner: DesktopRemoteOwner): {
export function applyRemoteDesktopStore(held: ClientLifecycleHeld, options: {
owner: DesktopRemoteOwner; expectedTokenFingerprint: string;
baseUrl: string; apiKey: string; mode: Desktop3pConfigMode; models: Desktop3pModelEntry[];
- /** Link-only local materialization: exact Child listener and a non-secret placeholder. */
- localListener?: boolean;
- /** One operation's consent to replace edited fields of this already-owned profile. */
- overwriteConflict?: boolean;
}): DesktopStoreResult {
return mutation(held, io => {
const { config, client } = currentConnection(options.owner);
noDisconnect(io);
if (client.pendingOperation) throw new DesktopStoreError("conflict");
const token = readServiceApiTokenState();
- const local = options.localListener === true;
- const validDestination = local
- ? client.transport === "link" && !!client.link && options.baseUrl === `http://127.0.0.1:${config.port}` && options.apiKey === "opencodex-loopback"
- : serviceApiTokenFingerprint(options.apiKey) === (token.kind === "present" ? token.fingerprint : "") && origin(options.baseUrl) === options.owner.serverUrl;
if (token.kind !== "present" || token.fingerprint !== options.expectedTokenFingerprint || token.fingerprint !== client.tokenFingerprint
- || !validDestination) throw new DesktopStoreError("conflict");
+ || serviceApiTokenFingerprint(options.apiKey) !== token.fingerprint || origin(options.baseUrl) !== options.owner.serverUrl) throw new DesktopStoreError("conflict");
if (config.clientIntegrations?.["claude-desktop"] === false) throw new DesktopStoreError("desired_disabled");
if (!options.models.length || !["static", "hybrid", "discovery"].includes(options.mode)) throw new DesktopStoreError("unsafe");
assertDesktop3pModelsValid(options.models);
const bundle = establish(io, options.owner, [token.fingerprint, ...serviceGenerations()], false)!;
const value = {
- inferenceProvider: "gateway", inferenceCredentialKind: "static", inferenceGatewayBaseUrl: local ? options.baseUrl : options.owner.serverUrl,
+ inferenceProvider: "gateway", inferenceCredentialKind: "static", inferenceGatewayBaseUrl: options.owner.serverUrl,
inferenceGatewayApiKey: options.apiKey, modelDiscoveryEnabled: options.mode !== "static",
...(options.mode === "discovery" ? {} : { inferenceModels: options.models }),
};
- artifact(io, bundle, value, "apply", token.fingerprint, bundle.state!.targetId, options.overwriteConflict);
+ artifact(io, bundle, value, "apply", token.fingerprint, bundle.state!.targetId);
cleanBackup(io, bundle, [token.fingerprint, ...serviceGenerations()]);
return success(io, bundle, "applied");
});
@@ -131,15 +123,6 @@ export function replaceRemoteDesktopCredential(held: ClientLifecycleHeld, option
const file = io.read(profilePath(bundle.paths.library, bundle.state!.targetId));
if (!file) throw new DesktopStoreError("unsafe");
const credential = profileCredential(file.value);
- const local = client.transport === "link" && !!client.link
- && file.value.inferenceGatewayBaseUrl === `http://127.0.0.1:${config.port}` && file.value.inferenceGatewayApiKey === "opencodex-loopback";
- if (local) {
- // artifact still verifies the recorded projection and selection. Only the service-key
- // generation changes; the local file never receives that secret during rotation.
- artifact(io, bundle, projection(file.value), "rotate", replacement);
- cleanBackup(io, bundle, [options.expectedTokenFingerprint, ...generations]);
- return success(io, bundle, "updated");
- }
if (!credential || credential.origin !== options.owner.serverUrl
|| ![options.expectedTokenFingerprint, replacement].includes(credential.fingerprint)) throw new DesktopStoreError("conflict");
artifact(io, bundle, { ...projection(file.value), inferenceGatewayApiKey: options.replacementKey }, "rotate", replacement);
diff --git a/src/cli/bundled-bun-diagnostic.ts b/src/cli/bundled-bun-diagnostic.ts
deleted file mode 100644
index d9d8535a83c..00000000000
--- a/src/cli/bundled-bun-diagnostic.ts
+++ /dev/null
@@ -1,91 +0,0 @@
-/**
- * `ocx doctor` check for a package install whose bundled Bun dependency never finished
- * installing (#8361).
- *
- * npm 12 blocks dependency lifecycle scripts by default. The `bun` npm package downloads nothing
- * in its tarball: its postinstall copies the selected `@oven/bun-*` executable into `bin/`. An
- * install that skipped that script succeeds and leaves a ~450-byte placeholder, so the launcher
- * fails later with "the `bun` dependency is not installed". Doctor is where an operator looks
- * after that failure, so it names the state and the exact command that repairs it.
- *
- * The check is observe-only and never fails the doctor run: a leftover PATH Bun or an explicit
- * `OPENCODEX_BUN_PATH` can still be serving requests, which makes this a degraded install rather
- * than an unusable surface.
- */
-import { dirname, resolve } from "node:path";
-import { fileURLToPath } from "node:url";
-import * as bunRuntime from "../lib/bun-runtime";
-import * as installDetection from "../update/install-detection.mjs";
-
-const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "..", "..");
-
-export const BUNDLED_BUN_MISSING_DETAIL = "the `bun` dependency is not installed";
-export const NPM_BUN_REPAIR_COMMAND = "npm install -g --allow-scripts=bun @bitkyc08/opencodex";
-
-type PackageInstaller = "npm" | "pnpm" | "bun" | "mise";
-
-export type BundledBunDiagnostic =
- | { status: "ready"; path: string }
- | { status: "not-applicable" }
- | { status: "missing"; installer: PackageInstaller; lines: string[] };
-
-export interface BundledBunDiagnosticDeps {
- platform?: NodeJS.Platform;
- bundledPath?: () => string | null;
- detectOwnership?: () => { installer: string; owner?: { tool: string } | null };
-}
-
-/** The command that reinstalls this package while allowing bun's postinstall. */
-export function bundledBunRepairCommand(installer: PackageInstaller, tool?: string | null): string | null {
- if (installer === "npm") return NPM_BUN_REPAIR_COMMAND;
- if (installer === "pnpm") return "pnpm add -g --allow-build=bun @bitkyc08/opencodex";
- if (installer === "bun") return "bun add -g @bitkyc08/opencodex";
- return tool ? `mise upgrade ${tool}` : null;
-}
-
-export function collectBundledBunDiagnostic(deps: BundledBunDiagnosticDeps = {}): BundledBunDiagnostic {
- const bundled = (deps.bundledPath ?? bunRuntime.bundledBunPath)();
- if (bundled) return { status: "ready", path: bundled };
-
- const ownership = (deps.detectOwnership
- ?? (() => installDetection.detectInstallOwnershipFromPath(PACKAGE_ROOT)))();
- const installer = ownership.installer;
- // Only the mise verdict carries an owner; the others have no tool to name.
- const ownerTool = "owner" in ownership ? ownership.owner?.tool ?? null : null;
- // A source checkout has no bundled dependency by design, and a standalone binary carries its
- // own runtime; neither is a skipped-postinstall symptom.
- if (installer !== "npm" && installer !== "pnpm" && installer !== "bun" && installer !== "mise") {
- return { status: "not-applicable" };
- }
-
- const repair = bundledBunRepairCommand(installer, ownerTool);
- const windows = (deps.platform ?? process.platform) === "win32";
- const lines = [
- ` !! ${BUNDLED_BUN_MISSING_DETAIL} (its postinstall was skipped or the dependency is incomplete)`,
- " Allow bun's own install script; npm 12 blocks lifecycle scripts by default,",
- " and --ignore-scripts or ignore-scripts=true in an npmrc blocks it on any npm",
- ];
- if (!repair) {
- lines.push(" This mise-owned installation has no verified tool name; upgrade it through mise.");
- return { status: "missing", installer, lines };
- }
- if (windows && installer === "npm") {
- // A running bun.exe holds node_modules/bun/bin open, so npm fails with EBUSY. The instruction
- // has to stop first; `ocx update` does this itself, but a manual reinstall does not.
- lines.push(" Stop the proxy first so Windows releases node_modules/bun/bin, then run:");
- lines.push(" ocx stop");
- lines.push(` ${repair}`);
- lines.push(" Restart afterwards: ocx service restart (or ocx start when unmanaged)");
- } else {
- lines.push(" Fix: reinstall with the package manager that owns this install:");
- lines.push(` ${repair}`);
- }
- return { status: "missing", installer, lines };
-}
-
-/** Render the doctor "Bundled Bun runtime" section lines. */
-export function formatBundledBunDiagnosticLines(diagnostic: BundledBunDiagnostic): string[] {
- if (diagnostic.status === "ready") return [` ok ready: ${diagnostic.path}`];
- if (diagnostic.status === "not-applicable") return [];
- return diagnostic.lines;
-}
diff --git a/src/cli/capabilities-access-remote.ts b/src/cli/capabilities-access-remote.ts
index d5773102f0e..add2702b22f 100644
--- a/src/cli/capabilities-access-remote.ts
+++ b/src/cli/capabilities-access-remote.ts
@@ -4,13 +4,12 @@ import type { Capability } from "./capability-types";
export const ACCESS_REMOTE_CAPABILITIES: readonly Capability[] = [
{
command: ["disconnect"], summary: "Disconnect a client or preserve orphaned token files for recovery.",
- usage: "ocx disconnect [--keep-catalog] [--recover-config --yes] [--json] | ocx disconnect --orphaned-token --yes [--json]",
+ usage: "ocx disconnect [--keep-catalog] [--json] | ocx disconnect --orphaned-token --yes [--json]",
routes: [],
flags: [
{ name: "--keep-catalog", value: "boolean", summary: "Keep the connected client's catalog during ordinary disconnect." },
- { name: "--recover-config", value: "boolean", summary: "With --yes, snapshot current Codex artifacts and recover only evidenced owned keys; unknown ownership still refuses." },
{ name: "--orphaned-token", value: "boolean", summary: "Preserve unowned service-token files in a private backup when disconnected." },
- { name: "--yes", value: "boolean", summary: "Explicitly confirm --recover-config or --orphaned-token recovery." },
+ { name: "--yes", value: "boolean", summary: "Explicitly confirm orphan-token recovery; requires --orphaned-token." },
{ name: "--json", value: "boolean", summary: "Emit the local result and backup location as JSON." },
],
mutates: true, json: "payload",
diff --git a/src/cli/capabilities-base.ts b/src/cli/capabilities-base.ts
index 5f2b8a4f47b..b79e3ba6a65 100644
--- a/src/cli/capabilities-base.ts
+++ b/src/cli/capabilities-base.ts
@@ -51,7 +51,7 @@ export const CAPABILITIES: readonly Capability[] = [
command: ["link","issue"],
summary: "Issue one link credential and record its tunnel metadata.",
routes: [{"method":"POST","path":"/api/link/issue"}],
- flags: [{"name":"--clients","value":"string","summary":"Initial device selection; defaults to Codex and Claude Code. Other Home client defaults remain inherited."},{"name":"--alias","value":"string","required":true,"summary":"SSH host alias for the linked machine."},{"name":"--tunnel-port","value":"number","required":true,"summary":"Remote loopback port for the reverse tunnel."},{"name":"--json","value":"boolean","summary":"Emit the issue result as JSON."}],
+ flags: [{"name":"--alias","value":"string","required":true,"summary":"SSH host alias for the linked machine."},{"name":"--tunnel-port","value":"number","required":true,"summary":"Remote loopback port for the reverse tunnel."},{"name":"--json","value":"boolean","summary":"Emit the issue result as JSON."}],
mutates: true,
json: "payload",
details: ["Human-only handoff: issues a credential and prints its plaintext once. Never capture it in an agent transcript. Requires loopback admin authority; tunnel port must be 1024-65535."],
@@ -752,7 +752,7 @@ export const CAPABILITIES: readonly Capability[] = [
flags: [{"name":"--json","value":"boolean","summary":"Emit the client rows or toggle result as JSON."}],
mutates: true,
json: "payload",
- details: ["Each on/off writes the Home's native switch, recording no route intent for the Home, and applies it through the client's runtime owner, including Claude Desktop's first-party transition; Children keep inheriting their saved default. Refused disables remain failures. On a Child it is refused with the exact Home command. Cursor status and local-installer are separate read-only subcommands; they do not toggle or install Cursor."],
+ details: ["Each toggle writes the selected client configuration through its runtime owner. Refused disables remain failures. Cursor status and local-installer are separate read-only subcommands; they do not toggle or install Cursor."],
usage: "ocx integration native [list] [--json]; ocx integration native [--json]",
},
{
@@ -762,7 +762,7 @@ export const CAPABILITIES: readonly Capability[] = [
flags: [{"name":"--model-names","value":"string","summary":"Hermes/Command Code apply or overwrite only: alias or routed; omission preserves managed policy."},{"name":"--client","value":"string","summary":"File integration ID, including gentle-shell; aside selects profiles."},{"name":"--profile","value":"number","summary":"Aside nonnegative integer account ID; requires --client aside."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."},{"name":"--overwrite-conflict","value":"boolean","summary":"Explicitly permit replacing a conflicting block."},{"name":"--op","value":"string","summary":"Operation ID; --op-id is an alias."},{"name":"--confirm-drift","value":"boolean","summary":"Explicitly allow replacing edits made after the snapshot."}],
mutates: true,
json: "payload",
- details: ["status/show/list reads all clients, one client or Aside profiles. history/journal reads rollback records; expired snapshots stay visible.","enable/disable requires --client; an omitted Aside --profile toggles all profiles. Each toggle is this Home's own decision, like the dashboard: once the Home uses route intent it is recorded so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). --overwrite-conflict applies only to enable.","restore requires --op (alias --op-id); --client requires --profile and only Aside supports that profile selector. --confirm-drift is an explicit user waiver, never auto-retried. Use client preview or restore --preview to inspect a change, history remove --op ID --yes to retire a journal row, and sync --client aside to refresh managed profiles."],
+ details: ["status/show/list reads all clients, one client or Aside profiles. history/journal reads rollback records; expired snapshots stay visible.","enable/disable requires --client; an omitted Aside --profile toggles all profiles. --overwrite-conflict applies only to enable.","restore requires --op (alias --op-id); --client requires --profile and only Aside supports that profile selector. --confirm-drift is an explicit user waiver, never auto-retried. Use client preview or restore --preview to inspect a change, history remove --op ID --yes to retire a journal row, and sync --client aside to refresh managed profiles."],
usage: "ocx integration client status [--client ] [--profile ] [--json]; ocx integration client history [--client ] [--profile ] [--json]; ocx integration client enable --client [--profile ] [--overwrite-conflict] [--model-names ] [--json]; ocx integration client disable --client [--profile ] [--json]; ocx integration client restore --op [--client aside --profile ] [--confirm-drift] [--json]",
},
{
@@ -770,8 +770,6 @@ export const CAPABILITIES: readonly Capability[] = [
summary: "Synchronize client catalogs, including Aside profiles through the running server's mutation owner.",
routes: [{ method: "POST", path: "/api/client-integrations/aside/sync" }],
flags: [
- { name: "--overwrite-conflict", value: "boolean", summary: "Allow conflicting route materialization only for this explicit sync." },
- { name: "--overwrite-client", value: "string", summary: "Restrict takeover to one client; requires --overwrite-conflict." },
{ name: "--local-only", value: "boolean", summary: "Do not fan out the Home sync to linked Children." },
{ name: "--json", value: "boolean", summary: "Emit the sync fan-out result as JSON." },
{ name: "--restart-codex", value: "boolean", summary: "Restart the Codex app-servers and fully quit and relaunch the Codex desktop app after a catalog, cache, or changed configuration write, on macOS, Linux and Windows." },
@@ -789,7 +787,7 @@ export const CAPABILITIES: readonly Capability[] = [
"After a successful Home sync, a connected Child skipped during fan-out, a failed or timed-out Child, or an unreadable link store yields partial exit 6. Skipped connected Children include an actionable hint; update an outdated ocx through its owning installer, with the Desktop tray as an option for Desktop installs. Intentional skips, including client-initiated links and --local-only, keep their existing exit behavior.",
"--expect-link/--expect-hub-key are checked under the lock that selects the sync path. A mismatch writes nothing, never falls back to local providers, exits 5, and ends stderr with {\"phase\":\"child_link_mismatch\"|\"hub_client_mismatch\",\"reason\":...}; reasons are disconnected, invalid_state, disconnect_pending, pending_operation, hub_client, link_client, other_link and other_key. Usage errors exit 2.",
],
- usage: "ocx sync [--expect-link | --expect-hub-key ] [--overwrite-conflict [--overwrite-client ]] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]",
+ usage: "ocx sync [--expect-link | --expect-hub-key ] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]",
},
{
command: ["agent","subagents","force"],
diff --git a/src/cli/capabilities-integrations.ts b/src/cli/capabilities-integrations.ts
index 5df20204675..338240652c8 100644
--- a/src/cli/capabilities-integrations.ts
+++ b/src/cli/capabilities-integrations.ts
@@ -89,7 +89,7 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
flags: [{"name":"--model-names","value":"string","summary":"Hermes/Command Code apply or overwrite only: alias or routed; omission preserves managed policy."},{"name":"--client","value":"string","summary":"Explicit file integration ID.","required":true},{"name":"--profile","value":"number","summary":"Aside nonnegative integer account ID; requires --client aside."},{"name":"--overwrite-conflict","value":"boolean","summary":"Explicitly permit replacing a conflicting block."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."},{"name":"--plan-fingerprint","value":"string","summary":"Optional versioned token from a matching preview; stale intent is refused without retry."},{"name":"--reasoning-default","value":"string","summary":"Repeat MODEL=EFFORT to replace the full Droid defaults map; exact model IDs, duplicate keys refused."},{"name":"--clear-reasoning-defaults","value":"boolean","summary":"Droid apply/overwrite only: send an empty map, distinct from omitted inherited defaults."}],
mutates: true,
json: "payload",
- details: ["A Home-only decision, like the dashboard toggle: once the Home uses route intent the server records it as this Home's override so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). On a Child a plain toggle is refused with the Home command. An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.","Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.","Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview.","Droid map omission preserves inheritance; supplied entries replace the full map, clear sends {}. Only Droid apply/overwrite accepts these options."],
+ details: ["An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.","Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.","Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview.","Droid map omission preserves inheritance; supplied entries replace the full map, clear sends {}. Only Droid apply/overwrite accepts these options."],
},
{
command: ["integration","client","disable"],
@@ -99,7 +99,7 @@ export const INTEGRATION_CAPABILITIES: readonly Capability[] = [
flags: [{"name":"--client","value":"string","summary":"Explicit file integration ID.","required":true},{"name":"--profile","value":"number","summary":"Aside nonnegative integer account ID; requires --client aside."},{"name":"--json","value":"boolean","summary":"Emit the result as JSON."},{"name":"--plan-fingerprint","value":"string","summary":"Optional versioned token from a matching preview; stale intent is refused without retry."}],
mutates: true,
json: "payload",
- details: ["A Home-only decision, like the dashboard toggle: once the Home uses route intent the server records it as this Home's override so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off changes those). On a Child a plain toggle is refused with the Home command. An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.","Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.","Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview."],
+ details: ["An omitted Aside profile toggles all Aside profiles. Ownership, snapshots, journal and conflict refusals remain server-owned.","Partial Aside failures remain failures after emitting the per-profile result. Never infer permission to overwrite from a refusal.","Bound Aside mutations require one explicit profile. Repeat the exact preview action/profile/options. The token is concurrency evidence, not authorization; stale state requires a new explicit preview."],
},
{
command: ["integration","client","restore"],
diff --git a/src/cli/capabilities-route-to.ts b/src/cli/capabilities-route-to.ts
deleted file mode 100644
index 002883519b3..00000000000
--- a/src/cli/capabilities-route-to.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-import type { Capability, CapabilityFlag } from "./capability-types";
-
-const device: CapabilityFlag = { name: "--device", value: "string", summary: "Home: alias, id, all or this. Child: this or its own link id only." };
-const json: CapabilityFlag = { name: "--json", value: "boolean", summary: "Emit the ocx.route-to/1 status matrix." };
-const overwrite: CapabilityFlag = { name: "--overwrite-conflict", value: "boolean", summary: "Explicitly permit replacing conflicting managed blocks." };
-const local: CapabilityFlag = { name: "--local", value: "boolean", summary: "Child only: opt out with off, or clear the opt-out with inherit." };
-
-export const ROUTE_TO_CAPABILITIES: readonly Capability[] = [
- {
- command: ["route", "to"],
- usage: "ocx route to [--device ] [--accounts ] [--model-names ] [--overwrite-conflict] [--local] [--json]",
- summary: "Set Home-default or per-device route intent, or opt out locally on a Child.",
- routes: [{ method: "PUT", path: "/api/route-to" }],
- flags: [device, json, overwrite, local,
- { name: "--accounts", value: "string", summary: "Aside on only: all, none, or comma-separated stable userIds; never u/N slots." },
- { name: "--model-names", value: "string", summary: "Hermes/Command Code on only: alias or routed model names." },
- ],
- mutates: true, json: "payload",
- details: ["Clients: codex, claude, claude-desktop, grok and the supported file integrations. Without --device a Home updates its default and materializes itself; all is only for status/sync.",
- "inherit requires --device unless clearing a Child --local opt-out. Child writes print the exact Home command with the Child link id; --local supports off/inherit only. --overwrite-conflict applies only to on or sync.",
- "On the Home, codex, claude, grok and claude-desktop follow the Home's own native switches: a Home default or --device home write for them writes that switch (Home inherit sets it to the default)."],
- },
- {
- command: ["route", "to", "status"],
- usage: "ocx route to status [--device ] [--json]",
- summary: "Read per-device route intent, its source and actual client apply outcomes.",
- routes: [{ method: "GET", path: "/api/route-to" }],
- flags: [device, json], mutates: false, json: "payload",
- details: ["On a Child this reads local receipts without a Home management relay. Intent and applied state are distinct; null applied means no recorded outcome.",
- "Sources: builtin, default, device, local (Child opt-out) and native (a Home row governed by the Home's own switch). An outcome recorded for an earlier intent reads stale."],
- },
- {
- command: ["route", "to", "sync"],
- usage: "ocx route to sync [--device ] [--overwrite-conflict] [--json]",
- summary: "Reapply route intent on selected devices, or locally on a Child.",
- routes: [{ method: "POST", path: "/api/route-to/sync" }],
- flags: [device, overwrite, json], mutates: true, json: "payload",
- details: ["No conflict retry or implicit overwrite. Child sync calls its local materializer and sends no Home management write.",
- "Home sync never changes the Home's native switches for codex, claude, grok or claude-desktop; it refreshes what each switch says."],
- },
-];
diff --git a/src/cli/capabilities.ts b/src/cli/capabilities.ts
index 8f1dda7f061..7a3c82376e7 100644
--- a/src/cli/capabilities.ts
+++ b/src/cli/capabilities.ts
@@ -13,7 +13,6 @@ import { INTEGRATION_CAPABILITIES } from "./capabilities-integrations";
import { OBSERVE_SYSTEM_CAPABILITIES } from "./capabilities-observe-system";
import { ACCESS_REMOTE_CAPABILITIES } from "./capabilities-access-remote";
import { LAB_CAPABILITIES } from "./capabilities-lab";
-import { ROUTE_TO_CAPABILITIES } from "./capabilities-route-to";
export const CAPABILITIES: readonly Capability[] = [
...BASE_CAPABILITIES,
@@ -24,7 +23,6 @@ export const CAPABILITIES: readonly Capability[] = [
...OBSERVE_SYSTEM_CAPABILITIES,
...ACCESS_REMOTE_CAPABILITIES,
...LAB_CAPABILITIES,
- ...ROUTE_TO_CAPABILITIES,
];
export { HEAD_CAPABILITIES } from "./capabilities-base";
diff --git a/src/cli/claude-desktop.ts b/src/cli/claude-desktop.ts
index 5e668fa7cdc..209c4a05c15 100644
--- a/src/cli/claude-desktop.ts
+++ b/src/cli/claude-desktop.ts
@@ -7,12 +7,11 @@ import { resolve } from "node:path";
import { getConfigDir, withConfigDirHardeningSettled } from "../config/paths";
import { loadConfig, mutatePersistedConfig, withConfigMutationLockSync } from "../config";
import { claudeDesktopIntegrationEnabledNow, setIntegrationEnabled } from "../codex/desired-state";
-import { setHomeIntegrationEnabled } from "../routing/home-route-capture";
import { readClientConnectionState, assertClientConnectionUnchanged, assertNoClientDisconnectPending, type ClientConnectionState } from "../client/state";
import { downloadDesktop3pModels, HubClientError, normalizeHubOrigin } from "../client/hub-client";
import { readServiceApiTokenState } from "../lib/service-secrets";
import { applyRemoteDesktopStore, type DesktopStoreResult } from "../claude/desktop-remote-store";
-import { assertClientLifecycleHeld, withClientLifecycle, withClientLifecycleSync, type ClientLifecycleHeld, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
+import { withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
import {
DESKTOP_FAMILIES,
moveDesktopRoute,
@@ -306,16 +305,12 @@ async function applyConnectedDesktopProfile(
}
/** Persist the operator's Desktop mode choice; a failed marker save never undoes the apply. */
-async function saveDesktopMode(mode: ClaudeDesktopMode, deps: ApplyProfileDeps, held?: ClientLifecycleHeld): Promise {
+async function saveDesktopMode(mode: ClaudeDesktopMode, deps: ApplyProfileDeps): Promise {
try {
- return await settleConfigRoot(configRoot(), () => {
- const save = () => {
- const outcome = mutatePersistedConfig(current => recordClaudeDesktopMode(current, mode));
- return outcome.status !== "unavailable";
- };
- if (held) { assertClientLifecycleHeld(held); return save(); }
- return withClientLifecycleSync(save, deps.lifecycleLockDeps);
- });
+ return await settleConfigRoot(configRoot(), () => withClientLifecycleSync(() => {
+ const outcome = mutatePersistedConfig(current => recordClaudeDesktopMode(current, mode));
+ return outcome.status !== "unavailable";
+ }, deps.lifecycleLockDeps));
} catch {
return false;
}
@@ -440,21 +435,8 @@ async function applyFirstPartyDesktop(
return { ok: false, path: "", reason: error instanceof Error ? error.message : "daemon apply failed" };
}
}
- // The offline path has no picker controller to serialize CLI/server writers. Keep L
- // across settings, gateway retirement, mode publication and any conditional rollback.
- try { return await withClientLifecycle(held => applyFirstPartyDesktopOffline(deps, held), deps.lifecycleLockDeps); }
- catch (error) {
- return { ok: false, path: "", reason: error instanceof Error && error.message === "client_lifecycle_busy"
- ? "client_lifecycle_busy" : "desktop_apply_failed" };
- }
-}
-
-async function applyFirstPartyDesktopOffline(deps: ApplyProfileDeps, held: ClientLifecycleHeld): Promise {
- assertClientLifecycleHeld(held);
- assertNoClientDisconnectPending();
- if (readClientConnectionState().kind !== "disconnected") return { ok: false, path: "", reason: "client_connection_invalid" };
const config = loadConfig();
- const desired = await withFirstPartySecretsSettled(getConfigDir(), () => setHomeIntegrationEnabled("claude-desktop", true));
+ const desired = await withFirstPartySecretsSettled(getConfigDir(), () => setIntegrationEnabled("claude-desktop", true));
if (!desired.ok) return "path" in desired ? desired : { ok: false, path: "", reason: desired.message };
// Establish the replacement before deleting the working gateway. A refused
// cleanup restores only our managed env keys, preserving unrelated settings.
@@ -470,8 +452,7 @@ async function applyFirstPartyDesktopOffline(deps: ApplyProfileDeps, held: Clien
const library = inspectDesktop3pConfigLibrary({ appliedFingerprint });
if (library.kind === "gateway_ours" || library.kind === "gateway_drifted") {
let removed: ReturnType;
- try { removed = await settleConfigRoot(configRoot(), () => removeDesktop3pStandardPivot({ appliedFingerprint: readConfigForAdmittedMutation().claudeCode?.desktopProfile?.appliedFingerprint ?? null, replaceWhileEnabled: true,
- lifecycleLockDeps: deps.lifecycleLockDeps, lifecycleHeld: held })); }
+ try { removed = await settleConfigRoot(configRoot(), () => removeDesktop3pStandardPivot({ appliedFingerprint: readConfigForAdmittedMutation().claudeCode?.desktopProfile?.appliedFingerprint ?? null, replaceWhileEnabled: true })); }
catch (error) {
removed = { ok: false, changed: false, kind: "write_failed", libraryPath: library.libraryPath,
reason: error instanceof WindowsSecretAclSettleTimeoutError ? "acl_settlement_timeout" : "desktop_mutation_failed" };
@@ -479,7 +460,7 @@ async function applyFirstPartyDesktopOffline(deps: ApplyProfileDeps, held: Clien
gatewayRemoved = removed.changed;
if (!removed.ok) {
const restored = removed.changed || !applied.changed || await rollbackDesktopFirstPartySettled(getConfigDir(), rollback).catch(() => false);
- const modeSaved = !removed.changed || await saveDesktopMode("first-party", deps, held);
+ const modeSaved = !removed.changed || await saveDesktopMode("first-party", deps);
const warning = [restored ? "" : "first-party settings rollback did not complete",
modeSaved ? "" : "first-party is active but its mode marker was not saved"].filter(Boolean).join("; ");
return { ok: false, path: library.selectedProfilePath ?? "", reason: removed.kind === "cleanup_incomplete" ? "gateway_cleanup_incomplete" : `gateway_profile_active:${removed.reason ?? removed.kind}`,
@@ -487,7 +468,7 @@ async function applyFirstPartyDesktopOffline(deps: ApplyProfileDeps, held: Clien
...(warning ? { warning } : {}) };
}
}
- const saved = await saveDesktopMode("first-party", deps, held);
+ const saved = await saveDesktopMode("first-party", deps);
modeCommitted = saved;
return {
ok: true,
@@ -540,7 +521,7 @@ export async function applyProfile(
// so a process crash cannot leave a gateway profile that startup immediately removes.
let committedIntent = false, savedProfile = false, gatewayApplied = false;
try {
- const desired = await settleConfigRoot(configRoot(), () => setHomeIntegrationEnabled("claude-desktop", true));
+ const desired = await settleConfigRoot(configRoot(), () => setIntegrationEnabled("claude-desktop", true));
if (!desired.ok) return { ok: false, path: "", reason: desired.message };
committedIntent = true;
const config = loadConfig();
diff --git a/src/cli/connect.ts b/src/cli/connect.ts
index 5271a540108..b3986f7df24 100644
--- a/src/cli/connect.ts
+++ b/src/cli/connect.ts
@@ -26,7 +26,6 @@ import { createSshRunner, type SshRunner } from "../link/ssh-runner";
import type { OrphanTunnelResult } from "../client/link-tunnel";
import { inspectRemoteDesktopStore } from "../claude/desktop-remote-store";
import type { OcxConnectedClientId } from "../types";
-import { ROUTE_CLIENT_IDS, isRouteClientId } from "../link/route-intent";
import {
CliUsageError,
csv,
@@ -241,8 +240,8 @@ export function collectClientConnectionStatus(
function parseClients(raw: string | undefined): OcxConnectedClientId[] {
const values = csv(raw) ?? ["codex", "claude"];
- if (values.length < 1 || values.some(value => !isRouteClientId(value))) {
- throw new CliUsageError(`--clients must contain route client ids: ${ROUTE_CLIENT_IDS.join(", ")}`, CONNECT_USAGE);
+ if (values.length < 1 || values.some(value => value !== "codex" && value !== "claude")) {
+ throw new CliUsageError("--clients must contain codex and/or claude", CONNECT_USAGE);
}
return values as OcxConnectedClientId[];
}
@@ -391,9 +390,6 @@ async function runHubConnect(argv: string[], deps: ClientCommandDeps): Promise client !== "codex" && client !== "claude")) {
- throw new CliUsageError("Hub connections support only codex and claude; use Link for other clients", CONNECT_USAGE);
- }
const catalogTimeoutSeconds = takeIntegerOption(args, "--catalog-timeout", { min: 1 });
if (catalogTimeoutSeconds !== undefined && catalogTimeoutSeconds > 120) {
throw new CliUsageError("--catalog-timeout must be an integer between 1 and 120", CONNECT_USAGE);
@@ -435,9 +431,9 @@ async function runHubConnect(argv: string[], deps: ClientCommandDeps): Promise 120) {
throw new CliUsageError("--catalog-timeout must be an integer between 1 and 120", CONNECT_USAGE);
@@ -473,7 +468,6 @@ async function runLinkConnect(argv: string[], deps: ClientCommandDeps): Promise<
managementUrl: `http://127.0.0.1:${rawPort}`,
credential: { kind: "link", ...credential },
selectedClients: clients,
- selectedClientsExplicit: rawClients !== undefined,
managementTransport: "direct",
transport: "link",
link: { tunnelPort: rawPort, linkId },
@@ -487,9 +481,9 @@ async function runLinkConnect(argv: string[], deps: ClientCommandDeps): Promise<
throw terminalSafeError(error);
});
const readiness = inspectInstalledCatalogReadiness(installedCatalogFileState(), deps.catalogProbeDeps ?? {});
- const report = connectCompletionReport(connection, connection.selectedClients, readiness);
+ const report = connectCompletionReport(connection, clients, readiness);
for (const line of report.lines) console.log(line);
- printCodexShimReadiness(connection.selectedClients);
+ printCodexShimReadiness(clients);
if (report.failure) throw new Error(report.failure);
}
@@ -544,10 +538,8 @@ export async function handleDisconnectCommand(
const keepCatalog = takeFlag(args, "--keep-catalog");
const wantsJson = takeFlag(args, "--json");
const orphanedToken = takeFlag(args, "--orphaned-token");
- const recoverConfig = takeFlag(args, "--recover-config");
const yes = takeFlag(args, "--yes");
rejectArgs(args, DISCONNECT_USAGE, { redactValues: true });
- if (recoverConfig && (!yes || orphanedToken)) throw new CliUsageError("--recover-config requires --yes and cannot be combined with --orphaned-token", DISCONNECT_USAGE);
if (orphanedToken) {
if (!yes || keepCatalog) throw new CliUsageError("--orphaned-token requires --yes and cannot be combined with --keep-catalog", DISCONNECT_USAGE);
const backup = await recoverOrphanedTokens({ ...deps.orphanRecoveryDeps, lifecycleLockDeps: deps.lifecycleLockDeps });
@@ -558,7 +550,7 @@ export async function handleDisconnectCommand(
]);
return;
}
- if (yes && !recoverConfig) throw new CliUsageError("--yes requires --orphaned-token or --recover-config", DISCONNECT_USAGE);
+ if (yes) throw new CliUsageError("--yes requires --orphaned-token", DISCONNECT_USAGE);
const teardown = await teardownClientLink({
readSidecar: readClientLinkState,
connectedLinkId: () => {
@@ -572,14 +564,7 @@ export async function handleDisconnectCommand(
knownHostsFile: deps.linkTeardownDeps?.knownHostsFile ?? linkKnownHostsPath(),
timeoutMs: deps.linkTeardownDeps?.timeoutMs,
});
- const result = await disconnectClient({ keepCatalog, recoverConfig }, deps);
- if (result.nativeIntentOnly) {
- printData(result, wantsJson, [
- "Not connected; restored the standalone native switches an interrupted Remote Link enrollment had changed.",
- ...(result.tokenRemoved ? ["Removed the service token that enrollment left behind."] : []),
- ]);
- return;
- }
+ const result = await disconnectClient({ keepCatalog }, deps);
const payload = {
...result,
homeRevoke: teardown.homeRevoke,
@@ -591,7 +576,6 @@ export async function handleDisconnectCommand(
};
printData(payload, wantsJson, [
"Disconnected locally; native Codex state was restored.",
- ...(result.recoveryBackupDir ? [`Codex recovery snapshot: ${result.recoveryBackupDir}/snapshot.json (private; retained for manual inspection).`] : []),
...(result.desktopRestoration === "standard_fallback"
? ["Previous Desktop settings were not recorded; the managed profile was switched to standard mode."] : []),
...(result.restartRequired ? ["Fully quit and reopen Claude Desktop; local cleanup cannot revoke an in-memory credential."] : []),
diff --git a/src/cli/dispatch.ts b/src/cli/dispatch.ts
index dba1a4abb35..1a9fd889871 100644
--- a/src/cli/dispatch.ts
+++ b/src/cli/dispatch.ts
@@ -21,11 +21,11 @@ import { printUnknownCommand } from "./help-recovery";
import {
HUB_GATED_SKIP_MESSAGE,
localClientSkipMessage,
+ setIntegrationEnabled,
shouldSyncCodexOnStart,
type LocalClientSkipReason,
} from "../codex/desired-state";
import { siblingSkipMessage } from "../codex/sibling-start";
-import { setHomeIntegrationEnabled, type HomeIntegrationResult } from "../routing/home-route-capture";
import { syncModelsToCodex } from "../codex/sync";
import { collectOrcaCodexHomeDiagnostic } from "../codex/home";
import { restoreNativeCodexAsync, type CodexNativeRestoreResult } from "../codex/inject";
@@ -89,12 +89,6 @@ export function selectDefaultGuiUrl(
return `http://${guiHost}:${live?.port ?? config.port ?? 10100}`;
}
-/** An unavailable route-intent file names the file to repair; the other refusals keep their reason. */
-function desiredStateRefusal(desired: Extract): string {
- return desired.reason === "unavailable" ? `Codex desired state was not saved (unavailable): ${desired.message}`
- : `Codex desired state was not saved (${desired.reason}).`;
-}
-
const commandRunners: Record = {
init: async () => {
const { runInit } = await import("./init");
@@ -179,13 +173,13 @@ const commandRunners: Record = {
if (!live) {
return emitBack(false, "No running proxy found. Run 'ocx start' — it injects opencodex automatically.", 1);
}
- const desired = setHomeIntegrationEnabled("codex", true);
+ const desired = setIntegrationEnabled("codex", true);
if (!desired.ok) {
- return emitBack(false, desiredStateRefusal(desired), desired.reason === "conflict" ? 2 : 1);
+ return emitBack(false, `Codex desired state was not saved (${desired.reason}).`, desired.reason === "conflict" ? 2 : 1);
}
const synced = await syncModelsToCodex(live.port);
if (synced.status === "skipped") {
- // `setHomeIntegrationEnabled` above just committed ON, so a skip here is NOT the toggle and
+ // `setIntegrationEnabled` above just committed ON, so a skip here is NOT the toggle and
// is not a competing writer either — on a hub it is the role gate. Telling the operator
// to "retry after the competing integration change finishes" sent them waiting for a
// writer that does not exist (#4236).
@@ -206,15 +200,15 @@ const commandRunners: Record = {
if (removeProviderTable && !restoreJson) {
console.log("⚠️ Removing [model_providers.opencodex] means conversations already tagged opencodex will stop opening.");
}
- const desired = setHomeIntegrationEnabled("codex", false);
+ const desired = setIntegrationEnabled("codex", false);
if (!desired.ok) {
if (restoreJson) {
// Machine-readable contract: every restore --json outcome emits one
// schema-complete envelope on stdout, including pre-machinery failures.
const { skippedRestoreEnvelope } = await import("../codex/inject");
- console.log(JSON.stringify(skippedRestoreEnvelope(false, desiredStateRefusal(desired))));
+ console.log(JSON.stringify(skippedRestoreEnvelope(false, `Codex desired state was not saved (${desired.reason}).`)));
} else {
- console.error(desiredStateRefusal(desired));
+ console.error(`Codex desired state was not saved (${desired.reason}).`);
}
return desired.reason === "conflict" ? 2 : 1;
}
@@ -758,12 +752,8 @@ const commandRunners: Record = {
return await handleCompanionCommand(deps.args.slice(1), { findLiveProxy: deps.findLiveProxy });
},
route: async deps => {
- if (deps.args[1] === "to") {
- const { handleRouteToCommand } = await import("./route-to");
- return await handleRouteToCommand(deps.args.slice(2), { findLiveProxy: deps.findLiveProxy });
- }
if (deps.args[1] !== "combo" && deps.args[1] !== "policy") {
- console.error("Usage: ocx route ");
+ console.error("Usage: ocx route ");
return 2;
}
if (deps.args[1] === "combo") {
diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts
index 04421d3ac65..3dea81c1020 100644
--- a/src/cli/doctor.ts
+++ b/src/cli/doctor.ts
@@ -8,7 +8,6 @@
* networking. See devlog/_plan/260630_wsl-account-autoswitch/30_*.
*/
import { cliCommandDoctorChecks, collectCliPathDiagnostics } from "./cli-path-diagnostics";
-import { collectBundledBunDiagnostic, formatBundledBunDiagnosticLines } from "./bundled-bun-diagnostic";
import { accessSync, constants, existsSync, readFileSync } from "node:fs";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
@@ -1608,16 +1607,6 @@ export async function runDoctor(args: string[] = []): Promise {
}
}
- // The bundled dependency is the install's own runtime, distinct from whatever Bun is running
- // doctor or serving the proxy. A skipped postinstall (#8361) is invisible here otherwise.
- // A source checkout or standalone binary has no bundled `bun` dependency, so the
- // section has nothing to say there and prints no heading.
- const bundledBunLines = formatBundledBunDiagnosticLines(collectBundledBunDiagnostic());
- if (bundledBunLines.length > 0) {
- console.log("\nBundled Bun runtime");
- for (const line of bundledBunLines) console.log(line);
- }
-
console.log("\nWHAM reachability");
const probe = await probeWham();
const detail = probe.status !== null ? `status=${probe.status}` : `error=${probe.classification}`;
diff --git a/src/cli/help-catalog.ts b/src/cli/help-catalog.ts
index 7743c2e0510..df29a851a4e 100644
--- a/src/cli/help-catalog.ts
+++ b/src/cli/help-catalog.ts
@@ -1,6 +1,5 @@
import { CAPABILITIES, type Capability } from "./capabilities";
import { CLI_COMMANDS, findCommand, type CliCommandEntry } from "./registry";
-import { EXPORT_CLIENT_IDS } from "../clients/client-ids";
export type HelpResolution =
| { kind: "entry"; entry: CliCommandEntry; canonicalName: string; capability?: Capability; children: readonly Capability[] }
@@ -89,16 +88,6 @@ export function resolveHelpPath(requested: readonly string[]): HelpResolution {
const capability = declared.find(candidate => candidate.command.length === path.length && startsWithPath(candidate.command, path));
const children = declared.filter(candidate => candidate.command.length > path.length && startsWithPath(candidate.command, path));
if (capability) return { kind: "capability", path, capability, children };
- // Clients and modes are operands of one capability, not registry leaves. Resolve their
- // help without multiplying the generated operating surface by every client/action pair.
- if (path[0] === "route" && path[1] === "to" && path.length >= 3
- && ["codex", "claude", "claude-desktop", "grok", ...EXPORT_CLIENT_IDS].includes(path[2])) {
- const owner = declared.find(candidate => candidate.command.join(" ") === "route to");
- if (owner && path.length === 3) return { kind: "prefix", path, children: ["on", "off", "inherit"].map(action => ({ ...owner, command: [...path, action] })) };
- if (owner && path.length === 4 && ["on", "off", "inherit"].includes(path[3])) {
- return { kind: "capability", path, capability: { ...owner, command: path }, children: [] };
- }
- }
if (children.length) return { kind: "prefix", path, children };
for (let length = Math.min(path.length - 1, MAX_DOCUMENTED_DEPTH); length > 1; length--) {
diff --git a/src/cli/help.ts b/src/cli/help.ts
index d86cc8464e2..67780c76753 100644
--- a/src/cli/help.ts
+++ b/src/cli/help.ts
@@ -86,7 +86,7 @@ Usage:
ocx fallback Turn model or provider fallback off per scope; per-device local fallback
ocx observe Logs, usage, storage, memory, and debug data
ocx inspect Effective config, catalog, analytics, pacing, client-config
- ocx route Routing features (to, combo, policy)
+ ocx route Routing features (combo, policy)
ocx logs [filters] Alias of ocx observe logs
ocx usage [--range ] [--provider ] [--model ] [--top <1-1000>]
Token and estimated-cost report (alias of ocx observe usage)
diff --git a/src/cli/inspect.ts b/src/cli/inspect.ts
index 8f8a85f8f09..4cabb0ceae8 100644
--- a/src/cli/inspect.ts
+++ b/src/cli/inspect.ts
@@ -109,17 +109,8 @@ async function nativeIntegration(argv: string[], deps: RuntimeApiDeps): Promise<
throw new CliUsageError(`expected on or off after ${action}`, USAGE);
}
- // A native switch is a Home-only decision. The native route (/api/native-integrations/)
- // writes the Home's switch, records no Home route intent, and runs the client's own transition
- // (Claude Desktop first-party included); Children keep inheriting their saved default. A Child
- // refuses here with the Home command for its own device.
- const { readLinkChildContext } = await import("./link-child");
- const routeDeps = deps as import("./route-to").RouteToDeps;
- if ((routeDeps.readChildContext ?? readLinkChildContext)()) {
- const { routeToAction } = await import("./route-to");
- await routeToAction([action, state, ...(wantsJson ? ["--json"] : [])], routeDeps);
- return;
- }
+ // Toggling rewrites the client's own config file, which is why each client has its own route
+ // rather than one route with a client parameter.
const result = await runtimeRequest(`/api/native-integrations/${action}`, {
method: "PUT",
headers: { "content-type": "application/json" },
diff --git a/src/cli/integrations.ts b/src/cli/integrations.ts
index 82e5eb9ffa2..09fa2fb392c 100644
--- a/src/cli/integrations.ts
+++ b/src/cli/integrations.ts
@@ -15,7 +15,6 @@ import {
import { clientIntegrationPath, validateAsideProfile } from "./integration-input";
import { asideProfileRecoveryLines } from "./aside-profile-recovery";
import type { IntegrationClientId } from "../integrations/registry";
-import type { RouteToDeps } from "./route-to";
const CLAUDE_USAGE = `Usage:
ocx claude config [status] [--json]
@@ -241,31 +240,10 @@ export async function handleClientIntegrationCommand(
const { handleIntegrationAsideSync } = await import("./integration-aside-sync");
return handleIntegrationAsideSync(argv.slice(1), deps);
}
- // Enable and disable are the Home's own decision, like the dashboard toggle: the server records
- // it so ocx sync keeps it, and linked Children keep their defaults (ocx route to on|off
- // changes those). On a Child the Home decides, so a plain toggle goes to route-to, which refuses
- // and prints the Home command.
- const toggle = ["enable", "disable"].includes(argv[0] ?? "");
- if (toggle && !argv.some(arg => ["--profile", "--preview", "--plan-fingerprint", "--reasoning-default", "--clear-reasoning-defaults"]
- .some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
- const { readLinkChildContext } = await import("./link-child");
- if (((deps as RouteToDeps).readChildContext ?? readLinkChildContext)()) {
- return runCliAction(async () => {
- const { integrationOption } = await import("./integration-input");
- const { routeToAction } = await import("./route-to");
- const args = argv.slice(1);
- const client = integrationOption(args, "--client");
- if (!client) throw new CliUsageError("--client is required", CLIENT_USAGE);
- await routeToAction([client, argv[0] === "enable" ? "on" : "off", ...args], deps);
- });
- }
- }
if (argv[0] === "preview" || argv.some(arg => ["--preview", "--plan-fingerprint", "--reasoning-default", "--clear-reasoning-defaults", "--model-names"]
.some(flag => arg === flag || arg.startsWith(`${flag}=`)))) {
const { handleIntegrationPreviewCommand } = await import("./integration-preview");
- const code = await handleIntegrationPreviewCommand(argv, deps, expectedClientId);
- if (code === 0 && toggle) console.error(HOME_ONLY_NOTE);
- return code;
+ return handleIntegrationPreviewCommand(argv, deps, expectedClientId);
}
return runCliAction(async () => {
const args = [...argv];
@@ -379,12 +357,9 @@ export async function handleClientIntegrationCommand(
? batch.results.map(row => `aside:${String(row.profileId)} ${String(row.message ?? (row.ok ? "updated" : "refused"))}${row.residual === true ? " Recovery did not finish." : ""}${typeof row.snapshotPath === "string" ? ` Backup: ${row.snapshotPath}` : ""}`)
: [String(batch.message ?? `${client} ${action}d.`)]);
if (batch.ok === false) throw new RuntimeApiError(batch.message ?? "Some Aside profiles could not be updated", 207, result);
- console.error(HOME_ONLY_NOTE);
});
}
-const HOME_ONLY_NOTE = "This changed this machine only; linked Children keep their route defaults (ocx route to on|off changes those).";
-
export const INTEGRATION_USAGE = { claude: CLAUDE_USAGE, grok: GROK_USAGE, client: CLIENT_USAGE };
const ZCODE_USAGE = `Usage:
diff --git a/src/cli/link-enrollment.ts b/src/cli/link-enrollment.ts
index f459c68ba47..adee88bfbae 100644
--- a/src/cli/link-enrollment.ts
+++ b/src/cli/link-enrollment.ts
@@ -207,10 +207,7 @@ export async function runLinkEnrollment(command: "probe" | "add", args: string[]
if (code === "child_preflight_output_limit") throw new Error("The Child's ocx config show --json output exceeded the Remote Link preflight limit; trim the Child's saved configuration and retry.");
if (code === "child_external_provider_conflict") throw new Error("The Child's ~/.codex/config.toml is already managed by another model provider or custom base URL; no Child files were modified.");
const phase = typeof code === "string" && Object.hasOwn(PHASES, code) ? PHASES[code] : undefined;
- // The server's hint is the Child's own bounded, redacted reason (#8357); a phase name
- // alone left every failure to SSH reproduction and source reading.
- const hint = record(error.body.error) && typeof error.body.error.hint === "string" ? error.body.error.hint : "";
- const message = phase ? "Remote Link failed during " + phase + (hint ? ": " + hint : "") : error.message;
+ const message = phase ? "Remote Link failed during " + phase : error.message;
const recovery = ephemeralFlow ? RECOVERY_EPHEMERAL : RECOVERY_FULL;
if (phase || error.body.residual === true) throw new Error(message + (error.body.residual === true ? ". " + recovery : ""));
}
diff --git a/src/cli/link.ts b/src/cli/link.ts
index 4ddc805d9fe..c7591093281 100644
--- a/src/cli/link.ts
+++ b/src/cli/link.ts
@@ -7,12 +7,10 @@ import {
import { TUNNEL_CAUSES } from "../link/tunnel-history";
import { isEpochMs, type LinkChildParity } from "../link/child-parity";
import { CODEX_SETTINGS_DIGEST_PATTERN } from "../link/codex-settings";
-import { isRouteClientId, parseRouteClientOutcomes, ROUTE_INTENT_DIGEST_PATTERN, routeClientOutcomesForPeer } from "../link/route-intent";
import type { LinkLocalPeer, LinkPeerStatus } from "../link/compatibility";
import { runLinkEnrollment } from "./link-enrollment";
import { runLinkCleanup, type LinkCleanupDeps } from "./link-cleanup";
import { assertSshAlias } from "../link/ssh-argv";
-import { boundHint } from "../link/ssh-runner";
import { linkStorePath } from "../link/paths";
import { isLinkPort } from "../link/ports";
import { readLinkStore, type LinkDirection, type LinkStore } from "../link/store";
@@ -38,7 +36,7 @@ export const LINK_USAGE = `Usage:
ocx link probe [--json]
ocx link add [--plant] [--expect-fingerprint SHA256:…] [--json]
ocx link port [--json]
- ocx link issue --alias --tunnel-port [--clients ] [--json]
+ ocx link issue --alias --tunnel-port [--json]
ocx link status [--json]
ocx link revoke --link-id [--purge] [--force --yes] [--json]
ocx link reconnect --link-id [--json]
@@ -248,22 +246,17 @@ async function linkRequest(path: string, init: RequestInit, deps: LinkCliDeps
} catch (error) {
if (error instanceof RuntimeApiError) {
// Never echo an API error body: issue responses contain a one-time data key. Only the error
- // code, the server's already-bounded hint and the residual flag survive (errorCodeBody).
+ // code survives, because callers branch on it and a code is never secret.
throw new RuntimeApiError(`Link management request failed (${error.status})`, error.status, errorCodeBody(error.body));
}
throw error;
}
}
-function errorCodeBody(body: unknown): { error: { code: string; hint?: string }; residual?: true } | null {
+function errorCodeBody(body: unknown): { error: { code: string }; residual?: true } | null {
if (!isRecord(body) || !isRecord(body.error)) return null;
const code = body.error.code;
- if (typeof code !== "string" || !/^[a-z_]{1,64}$/.test(code)) return null;
- // The hint is the only other field kept (#8357): the server builds it from a Child's last stderr
- // line, already bounded and redacted, and it is bounded and redacted again here. The rest of
- // the body is still never echoed.
- const hint = typeof body.error.hint === "string" ? boundHint(body.error.hint) : undefined;
- return { error: { code, ...(hint ? { hint } : {}) }, ...(body.residual === true ? { residual: true as const } : {}) };
+ return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code }, ...(body.residual === true ? { residual: true as const } : {}) } : null;
}
async function runPort(args: string[], deps: LinkCliDeps): Promise {
@@ -277,9 +270,6 @@ async function runPort(args: string[], deps: LinkCliDeps): Promise {
async function runIssue(args: string[], deps: LinkCliDeps): Promise {
takeJsonFlag(args);
const alias = takeOption(args, "--alias");
- const rawClients = takeOption(args, "--clients");
- const selectedClients = rawClients?.split(",");
- if (selectedClients && (!selectedClients.length || !selectedClients.every(isRouteClientId) || new Set(selectedClients).size !== selectedClients.length)) throw new CliUsageError("--clients needs unique supported client IDs", LINK_USAGE);
const tunnelPort = takeIntegerOption(args, "--tunnel-port", { min: 1024 });
if (!alias || tunnelPort === undefined || !isLinkPort(tunnelPort)) {
throw new CliUsageError("issue requires --alias and --tunnel-port", LINK_USAGE);
@@ -290,7 +280,7 @@ async function runIssue(args: string[], deps: LinkCliDeps): Promise {
const result = validateIssue(await linkRequest("/api/link/issue", {
method: "POST",
headers: { "content-type": "application/json" },
- body: JSON.stringify({ alias, tunnelPort, ...(selectedClients ? { selectedClients } : {}) }),
+ body: JSON.stringify({ alias, tunnelPort }),
}, deps));
console.log(JSON.stringify(result));
}
@@ -401,7 +391,7 @@ export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}):
});
}
-const PARITY_KEYS = ["lastApplyAt", "runtimeStartedAt", "appliedCodexSettingsDigest", "reportedAt", "appliedRouteIntentDigest", "routeClientOutcomes"] as const;
+const PARITY_KEYS = ["lastApplyAt", "runtimeStartedAt", "appliedCodexSettingsDigest", "reportedAt"] as const;
const WIRE_STATES = new Set(["connecting", "connected", "reconnecting", "failed", "idle"]);
const CAUSES = new Set(TUNNEL_CAUSES);
@@ -441,20 +431,10 @@ function validateParity(candidate: Record, index: number): Link
if (digest !== undefined && digest !== null && (typeof digest !== "string" || !CODEX_SETTINGS_DIGEST_PATTERN.test(digest))) {
throw new Error(`invalid link API response: link ${index} appliedCodexSettingsDigest`);
}
- let route: Pick = {};
- if (candidate.appliedRouteIntentDigest !== undefined || candidate.routeClientOutcomes !== undefined) {
- const routeDigest = candidate.appliedRouteIntentDigest;
- const outcomes = parseRouteClientOutcomes(candidate.routeClientOutcomes);
- if ((routeDigest !== null && (typeof routeDigest !== "string" || !ROUTE_INTENT_DIGEST_PATTERN.test(routeDigest))) || !outcomes) {
- throw new Error(`invalid link API response: link ${index} route intent`);
- }
- route = { appliedRouteIntentDigest: routeDigest, routeClientOutcomes: routeClientOutcomesForPeer(outcomes) };
- }
return {
lastApplyAt: time("lastApplyAt"),
runtimeStartedAt: time("runtimeStartedAt"),
appliedCodexSettingsDigest: typeof digest === "string" ? digest : null,
reportedAt: time("reportedAt"),
- ...route,
};
}
diff --git a/src/cli/registry.ts b/src/cli/registry.ts
index 0cb96d09997..a9f48283878 100644
--- a/src/cli/registry.ts
+++ b/src/cli/registry.ts
@@ -178,12 +178,12 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
},
{
name: "disconnect",
- usage: "ocx disconnect [--keep-catalog] [--recover-config --yes] [--json]",
+ usage: "ocx disconnect [--keep-catalog] [--json]",
summary: "Restore local client state offline and clear the remote-hub connection.",
},
{
name: "sync",
- usage: "ocx sync [--expect-link | --expect-hub-key ] [--overwrite-conflict [--overwrite-client ]] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]",
+ usage: "ocx sync [--expect-link | --expect-hub-key ] [--local-only] [--json] [--restart-codex] [--restart-app-server-only]",
summary: "Fetch provider models and inject them into Codex config.",
details: [
"A current Home catalog fans out to linked Children; --local-only disables fan-out and --json emits the result. Partial fan-out exits 6.",
@@ -431,8 +431,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
},
{
name: "route",
- usage: "ocx route ",
- summary: "Manage combo routing, routing profiles and per-device client intent.",
+ usage: "ocx route ",
+ summary: "Manage combo routing and routing profiles.",
},
{
name: "effort",
diff --git a/src/cli/route-to.ts b/src/cli/route-to.ts
deleted file mode 100644
index 4627ad94374..00000000000
--- a/src/cli/route-to.ts
+++ /dev/null
@@ -1,169 +0,0 @@
-import {
- CliUsageError, RuntimeApiError, printData, rejectArgs, runCliAction, runtimeRequest,
- type RuntimeApiDeps,
-} from "./runtime-api";
-import { integrationFlag, integrationOption } from "./integration-input";
-import { readLinkChildContext, linkChildRefusal, type LinkChildContext } from "./link-child";
-import { isRouteClientId, parseRouteClientIntent } from "../link/route-intent";
-import type { RouteToStatus } from "../routing/route-to-status";
-
-export const ROUTE_TO_USAGE = `Usage:
- ocx route to status [--device ] [--json]
- ocx route to sync [--device ] [--overwrite-conflict] [--json]
- ocx route to [--device ] [--accounts ] [--model-names ] [--overwrite-conflict] [--json]
- ocx route to --local [--device this] [--json]`;
-
-const loadLocal = () => import("../routing/route-to-local");
-export interface RouteToDeps extends RuntimeApiDeps {
- readChildContext?: () => LinkChildContext | null;
- loadLocal?: typeof loadLocal;
-}
-
-export function routeToStatusLines(status: RouteToStatus): string[] {
- return ["DEVICE CLIENT INTENT SOURCE APPLIED TARGET", ...status.devices.flatMap(device =>
- device.routeIntent === "unsupported" ? [`${device.alias || device.id} route intent unsupported (${device.reason ?? "unsupported"})`]
- : device.clients.map(row => [device.alias || device.id, row.client, row.intent, row.source,
- row.applied ?? "-", row.target ?? "-"].join(" "))),
- ...(status.syncs ?? []).map(row => `sync ${row.id}: ${row.state}${row.reason ? ` (${row.reason})` : ""}`),
- ...(status.deferred ? [`Local reconciliation deferred: Home intent or the Child catalog is unavailable${status.deferred.clients.length ? ` (${status.deferred.clients.join(", ")})` : ""}.`] : [])];
-}
-
-function accountsOption(raw: string | undefined): "all" | "none" | string[] | undefined {
- if (raw === undefined || raw === "all" || raw === "none") return raw;
- const values = raw.split(",").map(value => value.trim());
- if (!parseRouteClientIntent({ enabled: true, accounts: values }, "aside")
- || values.some(value => value === "all" || value === "none")) {
- throw new CliUsageError("--accounts requires all, none, or unique Aside userIds (not u/N slots)", ROUTE_TO_USAGE);
- }
- return values;
-}
-
-/** Shared with legacy toggle aliases, inside their existing error boundary. */
-export async function routeToAction(argv: string[], deps: RouteToDeps = {}): Promise {
- const args = [...argv];
- const json = integrationFlag(args, "--json");
- const local = integrationFlag(args, "--local");
- const overwriteConflict = integrationFlag(args, "--overwrite-conflict");
- const device = integrationOption(args, "--device");
- const accounts = accountsOption(integrationOption(args, "--accounts"));
- const modelNames = integrationOption(args, "--model-names");
- const subject = args.shift() ?? "status";
- const read = subject === "status";
- const sync = subject === "sync";
- const client = read || sync ? undefined : subject;
- const action = client === undefined ? undefined : args.shift();
- rejectArgs(args, ROUTE_TO_USAGE);
- if (device !== undefined && (!device.trim() || /[\x00-\x20\x7f]/.test(device))) {
- throw new CliUsageError("--device requires an alias, id, all or this", ROUTE_TO_USAGE);
- }
- if (client !== undefined && !isRouteClientId(client)) {
- throw new CliUsageError("Expected a supported route-to client", ROUTE_TO_USAGE);
- }
- if (client !== undefined && !["on", "off", "inherit"].includes(action ?? "")) {
- throw new CliUsageError("Expected a supported client and on, off or inherit", ROUTE_TO_USAGE);
- }
- if (client !== undefined && device === "all") {
- throw new CliUsageError("--device all applies only to status or sync; omit --device to change the Home default", ROUTE_TO_USAGE);
- }
- if (modelNames !== undefined && (modelNames !== "alias" && modelNames !== "routed")) {
- throw new CliUsageError("--model-names must be alias or routed", ROUTE_TO_USAGE);
- }
- if (accounts !== undefined && (client !== "aside" || action !== "on")) {
- throw new CliUsageError("--accounts requires aside on", ROUTE_TO_USAGE);
- }
- if (modelNames !== undefined && (action !== "on" || (client !== "hermes" && client !== "commandcode"))) {
- throw new CliUsageError("--model-names requires hermes or commandcode on", ROUTE_TO_USAGE);
- }
- if (overwriteConflict && !sync && action !== "on") {
- throw new CliUsageError("--overwrite-conflict applies only to on or sync", ROUTE_TO_USAGE);
- }
- if (local && (client === undefined || !["off", "inherit"].includes(action ?? "") || overwriteConflict)) {
- throw new CliUsageError("--local supports only client off or inherit", ROUTE_TO_USAGE);
- }
- if (action === "inherit" && device === undefined && !local) {
- throw new CliUsageError("inherit requires --device or --local", ROUTE_TO_USAGE);
- }
-
- // Detect Child before runtimeRequest: these operations must never enter its Home relay.
- const child = (deps.readChildContext ?? readLinkChildContext)();
- let result: unknown;
- let requestFailure: RuntimeApiError | undefined;
- if (child) {
- if (device !== undefined && device !== "this" && device !== child.linkId) {
- throw new CliUsageError("A Child can target only --device this or its own link id", ROUTE_TO_USAGE);
- }
- if (client !== undefined && !local) {
- const command = ["ocx route to", client, action, "--device", child.linkId,
- ...(accounts === undefined ? [] : ["--accounts", Array.isArray(accounts) ? accounts.join(",") : accounts]),
- ...(modelNames === undefined ? [] : ["--model-names", modelNames]),
- ...(overwriteConflict ? ["--overwrite-conflict"] : [])].join(" ");
- throw new RuntimeApiError(`${linkChildRefusal(child)}\nRun on the Home: ${command}`, 409, null);
- }
- const api = await (deps.loadLocal ?? loadLocal)();
- result = read ? await api.readLocalRouteToStatus(child)
- : sync ? await api.syncLocalRouteTo(child, overwriteConflict)
- : await api.setLocalRouteToOff(child, client!, action === "off");
- } else {
- if (local) throw new CliUsageError("--local requires a Remote Link Child", ROUTE_TO_USAGE);
- const body = { ...(device === undefined ? {} : { device }),
- ...(overwriteConflict ? { overwriteConflict: true } : {}) };
- try {
- result = read ? await runtimeRequest(`/api/route-to${device === undefined ? "" : `?device=${encodeURIComponent(device)}`}`, { redirect: "error" }, deps)
- : await runtimeRequest(sync ? "/api/route-to/sync" : "/api/route-to", {
- method: sync ? "POST" : "PUT", redirect: "error",
- headers: { "content-type": "application/json" },
- body: JSON.stringify(sync ? body : { client, action, ...body,
- ...(accounts === undefined ? {} : { accounts }), ...(modelNames === undefined ? {} : { modelNames }) }),
- }, deps);
- } catch (error) {
- if (!(error instanceof RuntimeApiError) || !error.body || typeof error.body !== "object"
- || !("schema" in error.body) || error.body.schema !== "ocx.route-to/1") throw error;
- requestFailure = error;
- result = error.body;
- }
- }
- const status = result as RouteToStatus;
- if (!status || status.schema !== "ocx.route-to/1" || !Array.isArray(status.devices) || status.devices.length === 0
- || (status.deferred !== undefined && (!status.deferred || status.deferred.reason !== "intent_or_catalog_pending"
- || !Array.isArray(status.deferred.clients) || status.deferred.clients.some(client => !isRouteClientId(client))))
- || (status.syncs !== undefined && (!Array.isArray(status.syncs) || status.syncs.some(row => !row || typeof row.id !== "string"
- || !["synced", "failed", "pending", "unsupported"].includes(row.state) || (row.reason !== undefined && typeof row.reason !== "string"))))
- || status.devices.some(row => !row || typeof row.id !== "string" || !Array.isArray(row.clients)
- || (row.routeIntent === undefined ? row.clients.length === 0
- : row.routeIntent !== "unsupported" || row.clients.length !== 0 || typeof row.reason !== "string")
- || (row.alias !== undefined && typeof row.alias !== "string")
- || row.clients.some(client => !client || !isRouteClientId(client.client)
- || (client.intent !== "on" && client.intent !== "off")
- || !["builtin", "default", "device", "native", "local"].includes(client.source)
- || (client.applied !== null && !["current", "stale", "not_installed", "conflict", "failed"].includes(client.applied))
- || (client.target !== undefined && typeof client.target !== "string")))) {
- throw new RuntimeApiError("Invalid route-to status response; outcome is unverified. Read status before retrying.", 502, null);
- }
- if (!read) {
- const requested = child?.linkId ?? (device === undefined || device === "this" ? "home" : device);
- const matching = status.devices.filter(row => row.id === requested || row.alias === requested);
- if ((requested !== "all" && matching.length !== 1)
- || (client !== undefined && !matching[0]?.clients.some(row => row.client === client))) {
- throw new RuntimeApiError("Invalid route-to status response; requested device/client is unverified. Read status before retrying.", 502, null);
- }
- }
- printData(status, json, routeToStatusLines(status));
- if (requestFailure) throw requestFailure;
- if (status.syncs?.some(row => row.state === "failed")) {
- throw new RuntimeApiError("Route sync failed for one or more devices. Read status before retrying.", 502, null);
- }
- if (sync && status.deferred) {
- throw new RuntimeApiError("Local route sync is deferred; not all client configurations were reconciled. Retry when the Home and Child catalog are available.", 503, null);
- }
- const failedClients = status.devices.flatMap(row => row.clients.filter(entry => entry.applied === "failed" || entry.applied === "conflict"));
- if (!read && failedClients.some(entry => client === undefined || entry.client === client)) {
- throw new RuntimeApiError("Route intent returned unresolved client failures or conflicts. Read status before retrying.", 409, null);
- }
- if (!read && client !== undefined && failedClients.length > 0) {
- console.error("Route status includes unresolved failures or conflicts for other clients; review their status.");
- }
-}
-
-export function handleRouteToCommand(argv: string[], deps: RouteToDeps = {}): Promise {
- return runCliAction(() => routeToAction(argv, deps));
-}
diff --git a/src/cli/sync-command.ts b/src/cli/sync-command.ts
index 5af086d9d59..1aec4298005 100644
--- a/src/cli/sync-command.ts
+++ b/src/cli/sync-command.ts
@@ -50,8 +50,7 @@ export async function runSyncCommand(deps: CliDispatchDeps): Promise {
if (expectation || clientState.kind === "connected") {
try {
const { syncConnectedClient } = await import("../client/connect");
- const result = await syncConnectedClient({ restartCodex, ...(expectation ? { expect: expectation } : {}),
- ...(syncArgs.includes("--overwrite-conflict") ? { overwriteConflict: true, ...(flags.overwriteClient ? { overwriteClient: flags.overwriteClient } : {}) } : {}) });
+ const result = await syncConnectedClient(expectation ? { restartCodex, expect: expectation } : { restartCodex });
if (expectation) {
// The root preflight skipped the shim repair for a pinned sync; the identity held, so
// run it now, as an unpinned sync would have.
@@ -76,13 +75,6 @@ export async function runSyncCommand(deps: CliDispatchDeps): Promise {
for (const line of describeHomeCodexSettings(settings.document.settings)) console.log(` ${terminalSafeText(line)}`);
}
}
- if (result.routeIntent) {
- for (const row of result.routeIntent.outcomes) {
- if (row.outcome === "current") continue;
- console.error(`Route to ${row.client}: ${row.outcome}.`);
- if (row.outcome !== "not_installed") process.exitCode = 1;
- }
- }
await handleConnectedSyncCatalogWrite(result, restartScope);
if (result.applyRecordFailed) {
console.error("The Home's Codex settings were applied, but the apply record could not be written; the client runtime was not restarted. Run 'ocx sync' again.");
@@ -199,17 +191,6 @@ export async function runSyncCommand(deps: CliDispatchDeps): Promise {
else if (result.reason) console.warn(`${label} integration was not refreshed: ${result.reason}${result.residual ? " Recovery did not finish." : ""}${result.snapshotPath ? ` Backup: ${result.snapshotPath}` : ""}`);
}
}
- if (synced.status !== "refused") {
- const { homeRouteIntentInUse, readRouteIntentState } = await import("../routing/route-intent-state");
- if (homeRouteIntentInUse(readRouteIntentState())) {
- const { syncHomeRouteTo } = await import("../routing/route-to-local");
- const { packageVersion } = await import("../lib/package-version");
- const outcomes = await syncHomeRouteTo({ ...deps.loadConfig(), ...(live ? { port: live.port } : {}) }, packageVersion("0.0.0"), syncArgs.includes("--overwrite-conflict"), flags.overwriteClient);
- for (const row of outcomes) {
- if (row.outcome !== "current" && row.outcome !== "not_installed") { console.error(`${row.client}: ${row.outcome}`); code = 1; }
- }
- }
- }
return code || (typeof process.exitCode === "number" ? process.exitCode : 0);
};
let code: number;
@@ -235,10 +216,7 @@ export async function runSyncCommand(deps: CliDispatchDeps): Promise {
return { state: "failed", reason: "platform-probe-failed", exit: null };
}
try {
- // An ephemeral ocx-child gets the plain catalog sync: it never reads route intent, so it
- // has nothing to overwrite and refuses the flags.
- const overwrite = link.childKind !== "ephemeral" && deps.args.includes("--overwrite-conflict");
- const result = await runFanoutProcess(buildExecArgv({ alias: link.alias, knownHostsFile: linkKnownHostsPath(), argv: remotePinnedSyncArgv(link.id, platform, overwrite, overwrite ? flags.overwriteClient : undefined) }), { timeoutMs: Math.max(0, deadline - Date.now()) });
+ const result = await runFanoutProcess(buildExecArgv({ alias: link.alias, knownHostsFile: linkKnownHostsPath(), argv: remotePinnedSyncArgv(link.id, platform) }), { timeoutMs: Math.max(0, deadline - Date.now()) });
return { ...classifySyncResult(result), exit: result.code };
} catch (error) {
if (error instanceof FanoutProcessError) return { state: error.reason === "deadline-exceeded" || error.reason === "termination-unconfirmed" ? "timed-out" : "failed", reason: error.reason, exit: null };
diff --git a/src/cli/sync-fanout.ts b/src/cli/sync-fanout.ts
index 587d8257424..bbbcdf487a5 100644
--- a/src/cli/sync-fanout.ts
+++ b/src/cli/sync-fanout.ts
@@ -2,7 +2,6 @@ import { compareStrictSemver, parseStrictSemver } from "../lib/strict-semver";
import type { LinkRecord, LinkStore } from "../link/store";
import type { LinkStatusDto } from "../link/status-projection";
import { LinkStatusError } from "./sync-link-status";
-import { ROUTE_OVERWRITE_MIN_CHILD_VERSION } from "../link/remote-windows";
export const REMOTE_LINK_SYNC_MESSAGE = "Remote Link Home catalog synchronized.";
export const STALE_REMOTE_CATALOG_MESSAGE = "Home unavailable; retained and applied the last-known-good remote catalog (stale).";
@@ -51,9 +50,6 @@ export function classifySyncResult(result: { code: number; stdout: string; stder
if (code === 121) {
if (last === "ocx-fanout:child-ocx-lacks-expect-link") return skipped("child-ocx-lacks-expect-link");
if (last === "ocx-fanout:child-ocx-version-unknown") return skipped("child-ocx-version-unknown");
- // --overwrite-conflict was refused before any sync: an older ocx would ignore it.
- if (last === "ocx-fanout:child-ocx-lacks-overwrite") return skipped("child-ocx-lacks-overwrite");
- if (last === "ocx-fanout:child-ephemeral-no-overwrite") return skipped("child-ephemeral-no-overwrite");
return failure("child-sync-failed");
}
const markers = lines.map(line => /^ocx-fanout:selected:(.+)$/.exec(line)).filter(match => match !== null);
@@ -91,8 +87,6 @@ export type FanoutDocument = { schema: "ocx.sync.fanout/1"; home: HomeResult; fa
function childSkipHint(reason: string | null, alias: string): string {
if (reason === "child-ocx-lacks-expect-link") return `Update OpenCodex on ${alias} to >= 2.93.0 using the installer that owns its selected ocx. For Desktop on headless Windows, use Check for Updates in the signed-in Desktop tray.`;
- if (reason === "child-ocx-lacks-overwrite") return `Update OpenCodex on ${alias} to >= ${ROUTE_OVERWRITE_MIN_CHILD_VERSION} to sync with --overwrite-conflict, or sync without it.`;
- if (reason === "child-ephemeral-no-overwrite") return `${alias} runs the ephemeral ocx-child, which has no route intent to overwrite; sync without --overwrite-conflict.`;
if (reason === "child-updating") return "Wait for the Child update to finish, then retry sync.";
if (reason === "child-link-mismatch:pending_operation" || reason === "child-link-mismatch:disconnect_pending") return "Wait for the Child's link operation to finish, then check its link and retry sync.";
if (reason === "child-link-mismatch:disconnected" || reason === "child-link-mismatch:other_link") return "Please re-link this Child to this Home, then retry sync.";
diff --git a/src/cli/sync-flags.ts b/src/cli/sync-flags.ts
index 5dec1fd6260..845b5e8068d 100644
--- a/src/cli/sync-flags.ts
+++ b/src/cli/sync-flags.ts
@@ -1,16 +1,10 @@
-import { isRouteClientId, type RouteClientId } from "../link/route-intent";
-export type SyncFanoutFlags = { ok: true; localOnly: boolean; json: boolean; overwriteClient?: RouteClientId } | { ok: false; message: string };
+export type SyncFanoutFlags = { ok: true; localOnly: boolean; json: boolean } | { ok: false; message: string };
export function parseSyncFanoutFlags(args: readonly string[]): SyncFanoutFlags {
- for (const flag of ["--local-only", "--json", "--overwrite-conflict"]) {
+ for (const flag of ["--local-only", "--json"]) {
if (args.filter(arg => arg === flag).length > 1 || args.some(arg => arg.startsWith(`${flag}=`))) {
return { ok: false, message: `ocx sync accepts ${flag} once without a value.` };
}
}
- const selectors = args.flatMap((arg, index) => arg === "--overwrite-client" ? [args[index + 1]] : arg.startsWith("--overwrite-client=") ? [arg.slice(19)] : []);
- if (selectors.length > 1 || (selectors.length && (!args.includes("--overwrite-conflict") || !isRouteClientId(selectors[0])))) {
- return { ok: false, message: "--overwrite-client needs one supported client and --overwrite-conflict." };
- }
- return { ok: true, localOnly: args.includes("--local-only"), json: args.includes("--json"),
- ...(selectors.length ? { overwriteClient: selectors[0] as RouteClientId } : {}) };
+ return { ok: true, localOnly: args.includes("--local-only"), json: args.includes("--json") };
}
diff --git a/src/client/connect.ts b/src/client/connect.ts
index 4bf34ec57e2..af6bf711eaf 100644
--- a/src/client/connect.ts
+++ b/src/client/connect.ts
@@ -2,12 +2,10 @@ import { createHash } from "node:crypto";
import {
existsSync,
lstatSync,
- mkdirSync,
readFileSync,
unlinkSync,
} from "node:fs";
import { hostname } from "node:os";
-import { dirname } from "node:path";
import { atomicWriteFile, loadConfig, withConfigMutationLockSync } from "../config";
import { claudeDesktopIntegrationEnabledNow } from "../codex/desired-state";
import { siblingOfLivePort, siblingSkipMessage } from "../codex/sibling-start";
@@ -30,11 +28,9 @@ import {
type CodexRoutingTarget,
} from "../codex/inject";
import {
- JOURNAL_PATH,
journalOwner,
restoreJournalState,
} from "../codex/journal";
-import { readCodexHomeJournal } from "../codex/codex-home-owner";
import { DEFAULT_CATALOG_PATH, getCodexHome } from "../codex/paths";
import {
readServiceApiTokenState,
@@ -100,15 +96,7 @@ import {
import { hubStateCachePath } from "./hub-state";
import { ClientLinkStateError, clearClientLinkState, readClientLinkState } from "./link-state";
import { isLinkPort } from "../link/ports";
-import { assertRouteIntentPort, clientSyncRuntimeGuard, ClientSyncRuntimeError } from "./sync-runtime";
-import { prepareConnectedRouteIntent, initialConnectRouteIntent, connectedRouteSelectedClients, applyConnectedRouteIntent } from "../integrations/route-intent-connected";
-import { clearRouteIntentReceipt, readRouteIntentReceiptStrict } from "../integrations/route-intent-state";
-import type { RouteIntentReceipt } from "../integrations/route-intent-contract";
-import { clearHomeRouteIntentRecord } from "./link-home";
-import { clearInitialRouteIntent, recordInitialRouteIntent } from "../integrations/route-intent-initial";
-import { cleanupConnectedRouteIntent } from "../integrations/route-intent-disconnect";
-import { nativeIntentOriginalsPath, readNativeIntentOriginals, restoreNativeIntentOriginals } from "./route-intent-native-originals";
-import { clearFileIntentOriginals } from "./route-intent-file-originals";
+import { clientSyncRuntimeGuard, ClientSyncRuntimeError } from "./sync-runtime";
class RotationRecoveryRequiredError extends Error {
constructor(message: string, options?: ErrorOptions) {
@@ -122,8 +110,6 @@ export interface ConnectOptions {
managementUrl?: string;
credential: OneTimeConnectCredential | LinkClientCredential;
selectedClients: OcxConnectedClientId[];
- /** Provisioning selection retained until Home revises that client's device policy. */
- selectedClientsExplicit?: boolean;
managementTransport: "direct" | "relay";
transport?: "hub" | "link";
link?: { tunnelPort: number; linkId: string };
@@ -619,9 +605,7 @@ async function connectClientUnlocked(
let priorCatalog: CatalogSnapshot | null = null;
let writtenCatalogFingerprint: string | null = null;
let injectionCommitted = false;
- let restoreNativeIntent = false;
let committed = false;
- let initialSelectionWritten = false;
// Before any catalog, journal, client-state or credential write: `CODEX_HOME` is shared with the
// live proxy a sibling instance runs beside, and connecting re-points it (`sibling-start.ts`).
if (siblingOfLivePort() !== null) throw new Error(siblingSkipMessage());
@@ -652,7 +636,6 @@ async function connectClientUnlocked(
// Only link mode needs the local port before the hub is contacted; hub mode keeps reading config
// after the catalog download, so its stderr order (catalog refusal first) is unchanged.
const earlyConfig = linkMode ? loadConfig() : undefined;
- if (earlyConfig) assertRouteIntentPort(earlyConfig.port);
if (earlyConfig && (!Number.isInteger(earlyConfig.port) || earlyConfig.port < 1 || earlyConfig.port > 65535)) {
throw new Error("link mode requires a valid local config port");
}
@@ -738,62 +721,37 @@ async function connectClientUnlocked(
assertClientCatalogCompatible(catalog.body, deps.catalogCompatibility);
writtenCatalogFingerprint = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
assertActiveConnectingState(persisted.fingerprint);
- mkdirSync(dirname(DEFAULT_CATALOG_PATH), { recursive: true });
atomicWriteFile(DEFAULT_CATALOG_PATH, catalog.body);
return sha256(catalog.body);
}), deps.lifecycleLockDeps);
const config = earlyConfig ?? loadConfig();
- const routeIntent = initialConnectRouteIntent(linkMode && linkMetadata
- ? await prepareConnectedRouteIntent({ transport: "link", link: linkMetadata, apiKeyId }, admissionToken, deps.linkHome,
- () => assertActiveConnectingState(persisted.fingerprint)) : undefined,
- options.selectedClients, options.selectedClientsExplicit);
- const selectedClients = connectedRouteSelectedClients(routeIntent, options.selectedClients);
const target = routingTarget(serverUrl, linkMode ? config.port : undefined, config);
// A link Child writes the Home's OpenCodex-owned Codex settings, not its own (#6957). A read
// that fails here keeps the local values; the next `ocx sync` applies the Home's.
// Read only: the record is written after the connection commits, so a rollback leaves none.
- const homeSettings = linkMode && linkMetadata && selectedClients.includes("codex")
+ const homeSettings = linkMode && linkMetadata && options.selectedClients.includes("codex")
? await fetchHomeCodexSettings({ transport: "link", link: linkMetadata, apiKeyId }, admissionToken, deps.linkHome)
: undefined;
const injectConfig = {
...(homeSettings?.kind === "fresh" ? overlayHomeCodexSettings(config, homeSettings.document.settings) : config),
syncResumeHistory: false,
};
- const beforeCodexWrite = () => {
- assertActiveConnectingState(persisted.fingerprint);
- if (routeIntent && (!connectedRouteSelectedClients(routeIntent, options.selectedClients).includes("codex")
- || loadConfig().port !== config.port)) throw new Error("Codex route intent or runtime changed; retry connect");
- };
- if (!options.noSync && routeIntent && selectedClients.includes("codex") && existsSync(getCodexHome())) {
- const { persistNativeRouteIntent } = await import("./route-intent-apply");
- // Armed before publication: the write-ahead originals record can exist even when the
- // config write or its bookkeeping throws, and rollback must still put the field back.
- restoreNativeIntent = true;
- withClientLifecycleSync(() => withConfigMutationLockSync(() => {
- beforeCodexWrite();
- // Home intent owns a selected installed client, including a standalone OFF. Keep
- // compensation field-scoped: a failed enrollment restores the recorded standalone
- // value, keeps a later edit by the person, and keeps disjoint config changes.
- persistNativeRouteIntent("codex", true, beforeCodexWrite, injectConfig, true);
- }), deps.lifecycleLockDeps);
- }
- const preflight = !selectedClients.includes("codex") || (routeIntent && !existsSync(getCodexHome()))
- ? { success: true, message: "Codex is not selected or installed" } : await injectCodexConfig(config.port, injectConfig, {
+ const preflight = await injectCodexConfig(config.port, injectConfig, {
validateOnly: true,
routingTarget: target,
catalogPath: DEFAULT_CATALOG_PATH,
journalOwner: { kind: "client", apiKeyId },
- beforeClientWrite: beforeCodexWrite,
+ beforeClientWrite: () => assertActiveConnectingState(persisted.fingerprint),
});
if (!preflight.success) throw new Error(preflight.message);
- if (!options.noSync && selectedClients.includes("codex") && (!routeIntent || existsSync(getCodexHome()))) {
+ if (!options.noSync && options.selectedClients.includes("codex")) {
const injected = await injectCodexConfig(config.port, injectConfig, {
routingTarget: target,
catalogPath: DEFAULT_CATALOG_PATH,
journalOwner: { kind: "client", apiKeyId },
- beforeClientWrite: beforeCodexWrite,
+ beforeClientWrite: () => assertActiveConnectingState(persisted.fingerprint),
});
if (!injected.success || injected.status === "skipped") throw new Error(injected.message);
injectionCommitted = true;
@@ -805,7 +763,7 @@ async function connectClientUnlocked(
serverUrl,
managementUrl,
managementTransport: options.managementTransport,
- selectedClients: [...selectedClients],
+ selectedClients: [...options.selectedClients],
tokenEnv: "OPENCODEX_API_AUTH_TOKEN",
apiKeyId,
tokenFingerprint: persisted.fingerprint,
@@ -821,10 +779,6 @@ async function connectClientUnlocked(
};
withClientLifecycleSync(() => withConfigMutationLockSync(() => {
assertActiveConnectingState(persisted.fingerprint);
- if (linkMetadata && options.selectedClientsExplicit) {
- recordInitialRouteIntent(linkMetadata.linkId, routeIntent?.document, options.selectedClients);
- initialSelectionWritten = true;
- }
clearClientConnectPending(persisted.fingerprint);
commitClientConnection(connection);
committed = true;
@@ -841,21 +795,6 @@ async function connectClientUnlocked(
console.warn("Warning: the Home's Codex settings were applied, but the apply record was not written; run 'ocx sync' to record it.");
}
}
- if (!options.noSync && routeIntent) {
- const appliedConnection = withClientLifecycleSync(() => {
- const current = readClientConnectionState();
- if (current.kind !== "connected" || !Bun.deepEquals(current.value, connection, true)) throw new Error("client_connection_changed");
- return current.value;
- }, deps.lifecycleLockDeps);
- try { await applyConnectedRouteIntent(connection, admissionToken, routeIntent, {
- codexApplied: injectionCommitted, lifecycleLockDeps: deps.lifecycleLockDeps, beforeWrite: () => {
- deps.signal?.throwIfAborted(); assertClientConnectionUnchanged(appliedConnection);
- const token = readServiceApiTokenState();
- if (token.kind !== "present" || token.fingerprint !== connection.tokenFingerprint) throw new Error("client_token_changed");
- },
- }, deps.linkHome); }
- catch { console.warn("Warning: local route intent needs reconciliation; run 'ocx route to sync'."); }
- }
return connection;
} catch (error) {
const rollbackFailures: string[] = [];
@@ -866,25 +805,10 @@ async function connectClientUnlocked(
try {
withClientLifecycleSync(() => withConfigMutationLockSync(() => {
assertNoClientDisconnectPending();
- if (initialSelectionWritten && !committed && linkMetadata) {
- try { clearInitialRouteIntent(linkMetadata.linkId); }
- catch { rollbackFailures.push("initial route selection rollback unavailable"); }
- }
- let nativeRestoreFailed = false;
- if (restoreNativeIntent && !committed && pendingConnectFingerprint) {
- const fingerprint = pendingConnectFingerprint;
- try { restoreNativeIntentOriginals(() => assertConnectingState(fingerprint)); }
- catch {
- // Like disconnect, keep the token (and the marker proving it is this attempt's) until the
- // standalone switches are back; ocx disconnect finishes this rollback.
- nativeRestoreFailed = true;
- rollbackFailures.push(`Standalone native switches were not restored, so the enrollment token was kept. Repair the config or ${nativeIntentOriginalsPath()}, then run ocx disconnect to finish the rollback.`);
- }
- }
if (priorCatalog && writtenCatalogFingerprint && !restoreCatalogSnapshot(priorCatalog, writtenCatalogFingerprint)) {
rollbackFailures.push("catalog rollback did not match the written artifact");
}
- if (pendingConnectFingerprint && !nativeRestoreFailed) {
+ if (pendingConnectFingerprint) {
const removed = removeServiceApiTokenFileIfOwned(pendingConnectFingerprint);
if (removed === "changed") rollbackFailures.push("service token changed during rollback");
// Final commit may fail after this attempt already cleared its marker under the same lock.
@@ -915,7 +839,6 @@ async function connectClientUnlocked(
}
export interface ConnectedSyncResult {
- routeIntent?: RouteIntentReceipt;
catalogWritten: boolean;
cacheSynced: boolean;
injected: boolean;
@@ -935,8 +858,6 @@ export interface ConnectedSyncResult {
export interface ConnectedSyncOptions {
restartCodex?: boolean;
- overwriteConflict?: boolean;
- overwriteClient?: import("../link/route-intent").RouteClientId;
/**
* #7694: refuse unless this is the connection found under the lock. A mismatch throws
* `ClientSyncExpectationError` before any request or write.
@@ -1009,21 +930,17 @@ async function syncConnectedClientUnlocked(
// with one it cannot. Refusing leaves the connection and the existing catalog exactly as
// they were, which is the known-good state.
if (downloaded) assertClientCatalogCompatible(downloaded.body, deps.catalogCompatibility);
- const routeIntent = await prepareConnectedRouteIntent(initial.connection, initial.token.token, deps.linkHome, () => {
- initial.assertRuntimeCurrent(); assertClientConnectionUnchanged(initial.connection);
- });
const next = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
initial.assertRuntimeCurrent();
assertClientConnectionUnchanged(initial.connection);
const token = readServiceApiTokenState();
if (token.kind !== "present" || token.fingerprint !== initial.token.fingerprint) throw new Error("client_token_changed");
- if (!downloaded) validLocalCatalog();
- else atomicWriteFile(DEFAULT_CATALOG_PATH, downloaded.body);
+ if (!downloaded) { validLocalCatalog(); return initial.connection; }
+ atomicWriteFile(DEFAULT_CATALOG_PATH, downloaded.body);
const updated = {
...initial.connection,
- selectedClients: [...connectedRouteSelectedClients(routeIntent, initial.connection.selectedClients)],
- ...(downloaded ? { catalogFingerprint: createHash("sha256").update(downloaded.body).digest("base64url"),
- catalogSyncedAt: (deps.now ?? (() => new Date()))().toISOString() } : {}),
+ catalogFingerprint: createHash("sha256").update(downloaded.body).digest("base64url"),
+ catalogSyncedAt: (deps.now ?? (() => new Date()))().toISOString(),
};
commitClientConnection(updated);
return updated;
@@ -1031,7 +948,6 @@ async function syncConnectedClientUnlocked(
// Read-only: injection invokes this while N/C are held. Acquiring L here would invert C→L.
const beforeClientWrite = () => {
initial.assertRuntimeCurrent();
- if (isLinkConnection(next)) assertRouteIntentPort(loadConfig().port);
assertClientConnectionUnchanged(next);
const token = readServiceApiTokenState();
if (next.pendingOperation || token.kind !== "present" || token.fingerprint !== next.tokenFingerprint) throw new Error("client_token_changed");
@@ -1041,13 +957,7 @@ async function syncConnectedClientUnlocked(
let homeSettings: HomeCodexSettingsSource | undefined;
let appliedRecord: HomeCodexSettingsRecord | undefined;
let applyRecordFailed = false;
- if (next.selectedClients.includes("codex") && (!routeIntent || existsSync(getCodexHome()))) {
- const beforeCodexWrite = () => {
- beforeClientWrite();
- if (routeIntent && !connectedRouteSelectedClients(routeIntent, next.selectedClients).includes("codex")) {
- throw new Error("Codex route intent changed; retry sync");
- }
- };
+ if (next.selectedClients.includes("codex")) {
let config = loadConfig();
// #6957: a link Child applies the Home's OpenCodex-owned Codex settings with the catalog.
if (isLinkConnection(next)) {
@@ -1061,15 +971,9 @@ async function syncConnectedClientUnlocked(
withClientWrite(() => withdrawChangingHomeCodexSettingsRecord(linkId, document));
}
}
- // Off persisted false. Reconcile the effective Home intent before the injector rereads
- // that durable gate, retaining local opt-outs and the owner/runtime/token commit fences.
- if (routeIntent) {
- const { persistNativeRouteIntent } = await import("./route-intent-apply");
- withClientLifecycleSync(() => persistNativeRouteIntent("codex", true, beforeCodexWrite, config, true), deps.lifecycleLockDeps);
- }
const result = await injectCodexConfig(config.port, { ...config, syncResumeHistory: false }, {
routingTarget: routingTarget(next.serverUrl, isLinkConnection(next) ? config.port : undefined, config), catalogPath: DEFAULT_CATALOG_PATH,
- journalOwner: { kind: "client", apiKeyId: next.apiKeyId }, beforeClientWrite: beforeCodexWrite,
+ journalOwner: { kind: "client", apiKeyId: next.apiKeyId }, beforeClientWrite,
});
if (!result.success || result.status === "skipped") throw new Error(result.message);
injected = true;
@@ -1088,9 +992,6 @@ async function syncConnectedClientUnlocked(
}
}
}
- const routeReceipt = await applyConnectedRouteIntent(next, initial.token.token, routeIntent,
- { beforeWrite: beforeClientWrite, codexApplied: injected, lifecycleLockDeps: deps.lifecycleLockDeps,
- overwriteConflict: options.overwriteConflict, overwriteClient: options.overwriteClient }, deps.linkHome);
const cacheSynced = withClientLifecycleSync(() => {
// Acquire K before the guarded C commit; never acquire K from inside C.
const owningHome = getCodexHome();
@@ -1100,11 +1001,10 @@ async function syncConnectedClientUnlocked(
return result.kind === "completed" && result.value;
}, deps.lifecycleLockDeps);
return {
- catalogWritten: downloaded !== undefined, cacheSynced, injected, stale: stale || routeReceipt?.catalogPending === true || routeIntent?.stale === true,
+ catalogWritten: downloaded !== undefined, cacheSynced, injected, stale,
...(homeSettings ? { homeSettings } : {}),
...(appliedRecord ? { appliedRecord } : {}),
...(applyRecordFailed ? { applyRecordFailed } : {}),
- ...(routeReceipt ? { routeIntent: routeReceipt } : {}),
connection: next,
};
}
@@ -1169,101 +1069,28 @@ function verifyDisconnectCatalog(receipt: DesktopDisconnectReceipt): void {
}
}
-function restoreConnectedCodex(connection: OcxClientConnectionConfig, receipt: DesktopDisconnectReceipt, recoverConfig = false): string | undefined {
- const inspection = readCodexHomeJournal(JOURNAL_PATH);
- const owner = journalOwner({ readOnly: true });
- const explicitOwner = inspection.kind === "read" ? inspection.journal.owner as Record | undefined : undefined;
- const invalidOwner = explicitOwner !== undefined && (!explicitOwner || typeof explicitOwner !== "object" || Array.isArray(explicitOwner)
- || !(explicitOwner.kind === "client" && typeof explicitOwner.apiKeyId === "string" && explicitOwner.apiKeyId.length > 0
- || explicitOwner.kind === "process" && typeof explicitOwner.pid === "number" && Number.isSafeInteger(explicitOwner.pid) && explicitOwner.pid > 0));
- if (inspection.kind === "unknown" || invalidOwner || (inspection.kind === "read" && owner === null)) {
- throw new Error("disconnect refused: Codex journal ownership is unknown; journal and connection credentials were retained");
- }
- // Deselection records desired state, not completed cleanup. A failed off still leaves
- // this client's journal to drain; an unrelated process journal grants no such authority.
- if (!connection.selectedClients.includes("codex") && owner?.kind !== "client") {
- if (inspection.kind === "missing" && isCodexRoutingInjected()) {
- throw new Error("disconnect refused: Codex routing is injected but no journal records the original state");
- }
- return;
- }
+function restoreConnectedCodex(connection: OcxClientConnectionConfig): void {
+ if (!connection.selectedClients.includes("codex")) return;
+ const owner = journalOwner();
if (owner && owner.kind === "client" && owner.apiKeyId !== connection.apiKeyId) {
throw new Error("disconnect refused: Codex journal ownership conflicts with the connected key");
}
if (owner !== null) {
- const restored = restoreJournalState({ preserveConfigEdits: true,
- ...(recoverConfig ? { recoverConfigForClient: connection.apiKeyId } : {}),
- beforeWrite: () => {
- const state = readClientConnectionState(), currentReceipt = readDesktopDisconnectReceipt();
- const token = readServiceApiTokenState();
- if (state.kind !== "connected" || JSON.stringify(state.value) !== JSON.stringify(connection)
- || currentReceipt.kind !== "valid" || JSON.stringify(currentReceipt.value) !== JSON.stringify(receipt)
- || token.kind !== "present" || token.fingerprint !== receipt.tokenFingerprint) throw new Error("client_disconnect_owner_changed");
- },
- });
- if (!restored.complete) throw new Error(
- "disconnect refused: Codex journal restore was partial; journal and connection credentials were retained. "
- + "Retry 'ocx disconnect --recover-config --yes' to privately back up current Codex config/profile and restore only evidenced OCX-owned fields (retain --keep-catalog if originally requested). "
- + "Unknown ownership and edited or unverified profiles remain refused.",
- );
- return restored.recoveryBackupDir;
+ if (!restoreJournalState().complete) throw new Error("disconnect refused: Codex journal restore was partial");
} else if (isCodexRoutingInjected()) {
throw new Error("disconnect refused: Codex routing is injected but no journal records the original state");
}
}
-/**
- * A disconnected machine with no unfinished disconnect can still hold the standalone native
- * switches an interrupted or failed enrollment displaced, and the token that enrollment kept
- * because it could not restore them. Plain `ocx disconnect` restores the switches, then removes
- * that token only while the pending-connect marker proves it is the enrollment's own.
- * `ocx disconnect --orphaned-token` never touches the record.
- */
-function drainLoneNativeIntentOriginals(lifecycleLockDeps?: ClientLifecycleLockDeps): {
- restored: true; tokenRemoved: boolean; catalogRemoved: false; catalogRestored: false; apiKeyId: string; restartRequired: false; nativeIntentOnly: true;
-} | null {
- const refused = (error: unknown) => new Error(`disconnect refused: the standalone native switches recorded in ${nativeIntentOriginalsPath()} were not restored (${error instanceof Error ? error.message : String(error)}). Repair the config or that record (remove it only after setting the switches by hand), then rerun ocx disconnect.`);
- return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
- const read = readDesktopDisconnectReceipt();
- if (read.kind === "unsafe" || (read.kind === "valid" && read.value.phase !== "complete")) return null;
- if (readClientConnectionState().kind !== "disconnected") return null;
- let present: boolean;
- try { present = readNativeIntentOriginals() !== null; } catch (error) { throw refused(error); }
- if (!present) return null;
- try {
- restoreNativeIntentOriginals(() => {
- assertNoClientDisconnectPending();
- if (readClientConnectionState().kind !== "disconnected") throw new Error("client_connection_changed");
- });
- } catch (error) { throw refused(error); }
- const token = readServiceApiTokenState();
- let tokenRemoved = false;
- if (token.kind === "present" && pendingClientConnectMayOwnToken(token.fingerprint)) {
- const removed = removeServiceApiTokenFileIfOwned(token.fingerprint);
- if (removed === "changed") throw new Error("client_token_changed");
- tokenRemoved = removed === "removed";
- clearClientConnectPending(token.fingerprint);
- }
- return { restored: true, tokenRemoved, catalogRemoved: false, catalogRestored: false, apiKeyId: "", restartRequired: false, nativeIntentOnly: true };
- }), lifecycleLockDeps);
-}
-
export async function disconnectClient(
- options: { keepCatalog?: boolean; expectedOwner?: DesktopRemoteOwner; recoverConfig?: boolean } = {},
+ options: { keepCatalog?: boolean; expectedOwner?: DesktopRemoteOwner } = {},
deps: Pick = {},
): Promise<{
restored: boolean; tokenRemoved: boolean; catalogRemoved: boolean; catalogRestored: boolean; apiKeyId: string;
desktopRestoration?: "owned_projection" | "standard_fallback" | "selection_preserved";
restartRequired: boolean;
- recoveryBackupDir?: string;
- /** Only standalone native switches an interrupted enrollment displaced were restored. */
- nativeIntentOnly?: true;
}> {
if (siblingOfLivePort() !== null) throw new Error(siblingSkipMessage());
- if (!options.expectedOwner) {
- const drained = drainLoneNativeIntentOriginals(deps.lifecycleLockDeps);
- if (drained) return drained;
- }
const keepCatalog = options.keepCatalog === true;
const prepared = withClientLifecycleSync(held => withConfigMutationLockSync(() => {
const read = readDesktopDisconnectReceipt();
@@ -1303,14 +1130,11 @@ export async function disconnectClient(
return { receipt, connection };
}), deps.lifecycleLockDeps);
- // This sibling step owns only file integrations; failures retain the prepared receipt/token.
- if (prepared.connection?.transport === "link" && !disconnectAtLeast(prepared.receipt, "desktop_restored")) {
- await cleanupConnectedRouteIntent(prepared.connection, prepared.receipt, deps.lifecycleLockDeps, options.recoverConfig === true);
- }
// The prepared receipt blocks even a sync queued for its actual N/C injection commit.
// Codex-only restore MUST stay outside L; it never invokes Desktop cleanup.
- const recoveryBackupDir = prepared.connection && !disconnectAtLeast(prepared.receipt, "desktop_restored")
- ? restoreConnectedCodex(prepared.connection, prepared.receipt, options.recoverConfig === true) : undefined;
+ if (prepared.connection && !disconnectAtLeast(prepared.receipt, "desktop_restored")) {
+ restoreConnectedCodex(prepared.connection);
+ }
return withClientLifecycleSync(held => withConfigMutationLockSync(() => {
const read = readDesktopDisconnectReceipt();
@@ -1361,17 +1185,6 @@ export async function disconnectClient(
}
advance("catalog_settled", { catalogAfter: catalogAfterState() });
} else verifyDisconnectCatalog(receipt);
- if (connection?.transport === "link" && !disconnectAtLeast(receipt, "removing_token")) {
- if (connection.link) clearInitialRouteIntent(connection.link.linkId);
- clearHomeRouteIntentRecord();
- // Its clients were restored by the route cleanup above, which phase gating never reruns.
- clearFileIntentOriginals();
- if (readRouteIntentReceiptStrict()?.linkId === connection.link?.linkId) clearRouteIntentReceipt();
- }
- // Standalone native switches come back before the token that displaced them is removed.
- // The record is machine-scoped, so any transport drains one a crashed enrollment left.
- // The locks and the receipt checked above already fence this; a retry is idempotent.
- if (!disconnectAtLeast(receipt, "removing_token")) restoreNativeIntentOriginals(() => {});
if (!disconnectAtLeast(receipt, "removing_token")) advance("removing_token");
const tokenRemoval = removeServiceApiTokenFileIfOwned(receipt.tokenFingerprint);
if (tokenRemoval === "changed") throw new Error("client_token_changed");
@@ -1395,7 +1208,6 @@ export async function disconnectClient(
requireDesktopResult(finishRemoteDesktopCleanup(held, receipt.owner));
if (receipt.phase !== "complete") advance("complete");
return {
- ...(recoveryBackupDir ? { recoveryBackupDir } : {}),
restored: true, tokenRemoved: tokenRemoval === "removed",
catalogRemoved: !keepCatalog, catalogRestored: !keepCatalog && receipt.catalogAfter?.kind === "file",
apiKeyId: receipt.owner.apiKeyId, restartRequired: desktop.restartRequired,
diff --git a/src/client/link-home.ts b/src/client/link-home.ts
index fede84eb9d7..3452e602ee1 100644
--- a/src/client/link-home.ts
+++ b/src/client/link-home.ts
@@ -7,8 +7,7 @@
* process that took the tunnel port never sees the key.
*/
import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
-import { dirname, join } from "node:path";
-import { parseHomeRouteIntentDocument, parseRouteClientOutcomes, ROUTE_CLIENT_IDS, routeIntentDigest, routeClientOutcomesForPeer, type HomeRouteIntentDocument, type RouteClientOutcome } from "../link/route-intent";
+import { join } from "node:path";
import { atomicWriteFile, isMissingPathError } from "../config/atomic-write";
import { readBoundedResponseBytes } from "../lib/bounded-body";
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
@@ -21,7 +20,6 @@ import {
import { LINK_CHILD_PARITY_PATH, type LinkChildParityReport } from "../link/child-parity";
import {
LINK_CODEX_SETTINGS_PATH,
- LINK_ROUTE_INTENT_PATH,
LINK_MANAGEMENT_PREFIX,
linkManagementReadAllowed,
} from "../link/management-routes";
@@ -338,22 +336,19 @@ export function recordHomeCodexSettings(
/**
* Tell the Home what this Child last applied and when its runtime started (OCX-6). Resolves to
- * `"accepted"`, `"unsupported"` for a Home that predates the route, `"schema-rejected"`
- * for a payload/schema refusal, or `"failed"` for every other refusal/transport failure.
+ * `"accepted"`, `"unsupported"` for a Home that predates the route, or `"failed"`.
*/
export async function sendLinkChildParityReport(
connection: LinkHomeConnection,
admissionKey: string,
report: LinkChildParityReport,
deps: LinkHomeDeps = {},
-): Promise<"accepted" | "unsupported" | "schema-rejected" | "failed"> {
+): Promise<"accepted" | "unsupported" | "failed"> {
let response: Response;
try {
const target = linkTarget(connection, admissionKey);
const destination = `http://127.0.0.1:${target.tunnelPort}${LINK_CHILD_PARITY_PATH}`;
- const body = JSON.stringify({ ...report, ...(report.routeClientOutcomes === undefined ? {} : {
- routeClientOutcomes: routeClientOutcomesForPeer(report.routeClientOutcomes),
- }) });
+ const body = JSON.stringify(report);
const request: RequestInit = {
method: "POST",
headers: {
@@ -372,88 +367,5 @@ export async function sendLinkChildParityReport(
}
try { await response.body?.cancel(); } catch { /* best effort */ }
if (response.status === 404) return "unsupported";
- if ([400, 413, 422].includes(response.status)) return "schema-rejected";
return response.status === 204 ? "accepted" : "failed";
}
-
-export interface HomeRouteIntentRecord {
- linkId: string;
- fetchedAt: string;
- appliedAt: string;
- document: HomeRouteIntentDocument;
- appliedRouteIntentDigest: string | null;
- routeClientOutcomes: RouteClientOutcome[];
-}
-export function homeRouteIntentPath(): string { return join(linkDir(), "home-route-intent.json"); }
-function completedRouteIntentDigest(document: HomeRouteIntentDocument, outcomes: RouteClientOutcome[]): string | null {
- return outcomes.length === ROUTE_CLIENT_IDS.length && new Set(outcomes.map(row => row.client)).size === ROUTE_CLIENT_IDS.length
- && outcomes.every(row => row.outcome === "current" && !(row.source === "local" && document.clients[row.client].enabled))
- ? routeIntentDigest(document) : null;
-}
-function parseHomeRouteIntentRecord(value: unknown, linkId: string): HomeRouteIntentRecord | null {
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
- const raw = value as Record;
- const fields = ["linkId", "fetchedAt", "appliedAt", "document", "appliedRouteIntentDigest", "routeClientOutcomes"];
- if (Object.keys(raw).length !== fields.length || Object.keys(raw).some(key => !fields.includes(key))
- || raw.linkId !== linkId || !/^lnk_[0-9a-f]{16}$/.test(linkId) || !isIsoTimestamp(raw.fetchedAt) || !isIsoTimestamp(raw.appliedAt)) return null;
- const document = parseHomeRouteIntentDocument(raw.document);
- const outcomes = parseRouteClientOutcomes(raw.routeClientOutcomes);
- if (!document || !outcomes || raw.appliedRouteIntentDigest !== completedRouteIntentDigest(document, outcomes)) return null;
- return { linkId, fetchedAt: raw.fetchedAt, appliedAt: raw.appliedAt, document,
- appliedRouteIntentDigest: raw.appliedRouteIntentDigest as string | null, routeClientOutcomes: outcomes };
-}
-export function readHomeRouteIntentRecord(linkId: string, path = homeRouteIntentPath()): HomeRouteIntentRecord | null {
- try {
- const stat = lstatSync(path);
- if (!stat.isFile() || stat.size > RECORD_MAX_BYTES) return null;
- return parseHomeRouteIntentRecord(JSON.parse(readFileSync(path, "utf8")), linkId);
- } catch { return null; }
-}
-export function writeHomeRouteIntentRecord(record: HomeRouteIntentRecord, path = homeRouteIntentPath()): void {
- const parsed = parseHomeRouteIntentRecord(record, record.linkId);
- if (!parsed) throw new Error("invalid Home route intent receipt");
- const dir = dirname(path);
- assertNotRealHomeUnderTest(dir);
- const body = `${JSON.stringify(parsed, null, 2)}\n`;
- if (Buffer.byteLength(body) > RECORD_MAX_BYTES) throw new Error("Home route intent receipt too large");
- mkdirSync(dir, { recursive: true, mode: 0o700 });
- if (process.platform === "win32") hardenSecretDir(dir, { required: true });
- else chmodSync(dir, 0o700);
- atomicWriteFile(path, body);
- if (process.platform !== "win32") chmodSync(path, 0o600);
-}
-export function clearHomeRouteIntentRecord(path = homeRouteIntentPath()): void {
- assertNotRealHomeUnderTest(dirname(path));
- try { unlinkSync(path); } catch (error) { if (!isMissingPathError(error)) throw error; }
-}
-/** Call only after the materializer has collected the outcomes; partial apply never claims the digest. */
-export function homeRouteIntentReceipt(
- linkId: string, document: HomeRouteIntentDocument, fetchedAt: string, appliedAt: string, routeClientOutcomes: RouteClientOutcome[],
- previous: HomeRouteIntentRecord | null = readHomeRouteIntentRecord(linkId),
-): HomeRouteIntentRecord {
- const digest = completedRouteIntentDigest(document, routeClientOutcomes);
- const unchanged = digest !== null && previous?.linkId === linkId && previous.appliedRouteIntentDigest === digest;
- return { linkId, document, fetchedAt, appliedAt: unchanged ? previous.appliedAt : appliedAt,
- appliedRouteIntentDigest: digest, routeClientOutcomes };
-}
-export type HomeRouteIntentFetch = { kind: "fresh"; document: HomeRouteIntentDocument } | { kind: "unsupported" } | { kind: "failed"; reason: string };
-export async function fetchHomeRouteIntent(connection: LinkHomeConnection, admissionKey: string, deps: LinkHomeDeps = {}): Promise {
- let response: Response;
- try {
- const target = linkTarget(connection, admissionKey);
- const destination = `http://127.0.0.1:${target.tunnelPort}${LINK_ROUTE_INTENT_PATH}`;
- const request: RequestInit = { method: "GET", headers: { accept: "application/json", authorization: `Bearer ${admissionKey}` },
- redirect: "manual", signal: AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS) };
- response = deps.fetchImpl ? await deps.fetchImpl(destination, request) : await fetchBoundLinkRelay(target, destination, request);
- } catch { return { kind: "failed", reason: "the Home route intent is unreachable" }; }
- if (!response.ok) {
- try { await response.body?.cancel(); } catch { /* best effort */ }
- return response.status === 404 ? { kind: "unsupported" } : { kind: "failed", reason: `the Home answered HTTP ${response.status}` };
- }
- try {
- const read = await readBoundedResponseBytes(response, { maxBytes: SETTINGS_MAX_BYTES, signal: AbortSignal.timeout(deps.timeoutMs ?? READ_TIMEOUT_MS) });
- if (read.oversized) return { kind: "failed", reason: "the Home route intent exceeded the allowed size" };
- const document = parseHomeRouteIntentDocument(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(read.bytes)));
- return document ? { kind: "fresh", document } : { kind: "failed", reason: "the Home returned an unreadable route intent document" };
- } catch { return { kind: "failed", reason: "the Home returned an unreadable route intent document" }; }
-}
diff --git a/src/client/link-join.ts b/src/client/link-join.ts
index f70425d965f..83c6163fc7a 100644
--- a/src/client/link-join.ts
+++ b/src/client/link-join.ts
@@ -316,21 +316,14 @@ export async function joinHome(deps: ClientLinkJoinDeps, input: { alias: string
const thisAlias = localAlias(deps);
let result: SshRunResult;
try {
- const issue = (selectedClients?: OcxConnectedClientId[]) => deps.runner.run(
+ result = await deps.runner.run(
buildExecArgv({
alias: input.alias,
- argv: remoteOcxArgv(["link", "issue", "--alias", thisAlias, "--tunnel-port", String(tunnelPort), ...(selectedClients ? ["--clients", selectedClients.join(",")] : []), "--json"]),
+ argv: remoteOcxArgv(["link", "issue", "--alias", thisAlias, "--tunnel-port", String(tunnelPort), "--json"]),
knownHostsFile: deps.knownHostsFile,
}),
{ timeoutMs: JOIN_REVOKE_TIMEOUT_MS },
);
- result = await issue(deps.selectedClients);
- // The older CLI rejects unknown arguments before issuing a key. Retry only that
- // definite no-write result; keep the selection for the Child's existing fallback.
- if (deps.selectedClients && result.code === 2 && !result.stdout.trim()
- && /^(?:Error: )?Unexpected argument\(s\): --clients(?:\s|$)/m.test(result.stderr)) {
- result = await issue();
- }
} catch (error) {
throw new ClientLinkJoinError("join_issue_failed", undefined, sshRunnerErrorHint(error));
}
@@ -390,7 +383,6 @@ export async function joinHome(deps: ClientLinkJoinDeps, input: { alias: string
transport: "link",
link: { tunnelPort, linkId: issued.linkId },
selectedClients: deps.selectedClients ?? ["codex", "claude"],
- selectedClientsExplicit: deps.selectedClients !== undefined,
managementTransport: "direct",
}, {
fetchImpl: deps.fetchImpl,
diff --git a/src/client/link-parity-reporter.ts b/src/client/link-parity-reporter.ts
index 7fa14024e88..ff4d4e1f437 100644
--- a/src/client/link-parity-reporter.ts
+++ b/src/client/link-parity-reporter.ts
@@ -8,12 +8,9 @@
* predates the route (404) is not asked again. Timers are unref'd and never keep the process alive.
*/
import { codexSettingsDigest } from "../link/codex-settings";
-import { routeClientOutcomesForPeer, routeIntentDigest, type RouteClientId } from "../link/route-intent";
-import type { RouteIntentReceipt } from "../integrations/route-intent-contract";
-import { effectiveRouteIntent, readLocalRouteIntentOff, readRouteIntentReceiptStrict } from "../integrations/route-intent-state";
import type { LinkChildParityReport } from "../link/child-parity";
import type { OcxClientConnectionConfig } from "../types";
-import { readHomeCodexSettingsRecord, readHomeRouteIntentRecord, sendLinkChildParityReport, type HomeCodexSettingsRecord, type HomeRouteIntentRecord } from "./link-home";
+import { readHomeCodexSettingsRecord, sendLinkChildParityReport, type HomeCodexSettingsRecord } from "./link-home";
export const LINK_PARITY_RETRY_MS = [2_000, 5_000, 15_000, 30_000, 60_000] as const;
export const LINK_PARITY_PERIOD_MS = 60_000;
@@ -28,7 +25,6 @@ function nextReportSequence(): number {
export interface LinkParityReporter {
start(): void;
stop(): void;
- requestReport(): void;
}
export interface LinkParityReporterDeps {
@@ -37,9 +33,6 @@ export interface LinkParityReporterDeps {
/** ISO start time of this runtime. */
runtimeStartedAt: string;
readRecord?: (linkId: string) => HomeCodexSettingsRecord | null;
- readRouteRecord?: (linkId: string) => HomeRouteIntentRecord | null;
- readCommonReceipt?: () => RouteIntentReceipt | null;
- readLocalOff?: () => RouteClientId[];
send?: typeof sendLinkChildParityReport;
setTimer?: (callback: () => void, ms: number) => unknown;
clearTimer?: (timer: unknown) => void;
@@ -51,24 +44,18 @@ export function linkChildParityReport(
runtimeStartedAt: string,
record: HomeCodexSettingsRecord | null,
sequence: number = nextReportSequence(),
- routeRecord?: HomeRouteIntentRecord | null,
): LinkChildParityReport | null {
const linkId = connection.link?.linkId;
const started = Date.parse(runtimeStartedAt);
if (!linkId || !Number.isFinite(started)) return null;
const applied = record ? Date.parse(record.appliedAt) : Number.NaN;
- const report: LinkChildParityReport = Number.isFinite(applied)
+ return Number.isFinite(applied)
? { linkId, lastApplyAt: applied, runtimeStartedAt: started, appliedCodexSettingsDigest: codexSettingsDigest(record!.document.settings), sequence }
: { linkId, lastApplyAt: null, runtimeStartedAt: started, appliedCodexSettingsDigest: null, sequence };
- return routeRecord?.linkId === linkId ? { ...report, appliedRouteIntentDigest: routeRecord.appliedRouteIntentDigest,
- routeClientOutcomes: routeClientOutcomesForPeer(routeRecord.routeClientOutcomes) } : report;
}
export function createLinkParityReporter(deps: LinkParityReporterDeps): LinkParityReporter {
const readRecord = deps.readRecord ?? readHomeCodexSettingsRecord;
- const readRouteRecord = deps.readRouteRecord ?? readHomeRouteIntentRecord;
- const readCommonReceipt = deps.readCommonReceipt ?? readRouteIntentReceiptStrict;
- const readLocalOff = deps.readLocalOff ?? readLocalRouteIntentOff;
const send = deps.send ?? sendLinkChildParityReport;
const setTimer = deps.setTimer ?? ((callback: () => void, ms: number) => {
const timer = setTimeout(callback, ms);
@@ -79,8 +66,6 @@ export function createLinkParityReporter(deps: LinkParityReporterDeps): LinkPari
let timer: unknown;
let stopped = true;
let failures = 0;
- let inFlight = false;
- let rerun = false;
const schedule = (ms: number) => {
if (stopped) return;
@@ -90,51 +75,20 @@ export function createLinkParityReporter(deps: LinkParityReporterDeps): LinkPari
const tick = async (): Promise => {
timer = undefined;
if (stopped) return;
- if (inFlight) { rerun = true; return; }
- inFlight = true;
- let delay: number = LINK_PARITY_PERIOD_MS;
- try {
const key = deps.linkKey();
const linkId = deps.connection.link?.linkId;
let record: HomeCodexSettingsRecord | null = null;
try { record = linkId ? readRecord(linkId) : null; } catch { record = null; }
- let routeRecord: HomeRouteIntentRecord | null = null;
- try {
- routeRecord = linkId ? readRouteRecord(linkId) : null;
- const common = readCommonReceipt();
- const effective = common ? effectiveRouteIntent(common.document, undefined, readLocalOff()) : null;
- if (!routeRecord || routeRecord.linkId !== linkId || !common || common.linkId !== linkId || common.catalogPending
- || routeIntentDigest(common.document) !== routeIntentDigest(routeRecord.document)
- || common.appliedRouteIntentDigest !== routeRecord.appliedRouteIntentDigest
- || !Bun.deepEquals(common.clients, effective?.clients, true) || !Bun.deepEquals(common.sources, effective?.sources, true)
- || !Bun.deepEquals(routeClientOutcomesForPeer(common.outcomes), routeClientOutcomesForPeer(routeRecord.routeClientOutcomes), true)) {
- routeRecord = null;
- }
- } catch { routeRecord = null; }
- const base = linkChildParityReport(deps.connection, deps.runtimeStartedAt, record, nextReportSequence(), routeRecord);
- // Omission is not a withdrawal for peers retaining their last route confirmation.
- const report = base && !routeRecord ? { ...base, appliedRouteIntentDigest: null, routeClientOutcomes: [] } : base;
- let outcome = key && report ? await send(deps.connection, key, report).catch(() => "failed" as const) : "failed";
- if (key && report && outcome === "schema-rejected" && report.routeClientOutcomes !== undefined) {
- const { appliedRouteIntentDigest: _digest, routeClientOutcomes: _outcomes, ...baseReport } = report;
- outcome = await send(deps.connection, key, baseReport).catch(() => "failed" as const);
- }
+ const report = linkChildParityReport(deps.connection, deps.runtimeStartedAt, record);
+ const outcome = key && report ? await send(deps.connection, key, report).catch(() => "failed" as const) : "failed";
if (outcome === "unsupported") { stopped = true; return; }
if (outcome === "accepted") {
failures = 0;
+ schedule(LINK_PARITY_PERIOD_MS);
return;
}
failures = Math.min(failures + 1, LINK_PARITY_RETRY_MS.length - 1);
- delay = LINK_PARITY_RETRY_MS[failures]!;
- } catch {
- failures = Math.min(failures + 1, LINK_PARITY_RETRY_MS.length - 1);
- delay = LINK_PARITY_RETRY_MS[failures]!;
- } finally {
- inFlight = false;
- const immediate = rerun;
- rerun = false;
- schedule(immediate ? 0 : delay);
- }
+ schedule(LINK_PARITY_RETRY_MS[failures]!);
};
return {
@@ -148,14 +102,6 @@ export function createLinkParityReporter(deps: LinkParityReporterDeps): LinkPari
stopped = true;
if (timer !== undefined) clearTimer(timer);
timer = undefined;
- rerun = false;
- },
- requestReport() {
- if (stopped) return;
- if (timer !== undefined) clearTimer(timer);
- timer = undefined;
- if (inFlight) rerun = true;
- else void tick();
},
};
}
diff --git a/src/client/route-intent-apply.ts b/src/client/route-intent-apply.ts
deleted file mode 100644
index 51338048cae..00000000000
--- a/src/client/route-intent-apply.ts
+++ /dev/null
@@ -1,293 +0,0 @@
-/** Production native adapters for the common Home/Child local intent controller. */
-import { existsSync } from "node:fs";
-import { homedir } from "node:os";
-import { join } from "node:path";
-import type { ExportModel } from "../clients/config-export";
-import { loadConfig, mutatePersistedConfig, withConfigMutationLockSync } from "../config";
-import { readConfigForAdmittedMutation } from "../config/persisted-mutation";
-import { commitClaudeCodeBlock } from "../claude/claude-code-block";
-import { localInferenceDestination } from "../lib/local-destinations";
-import { persistCommittedDesktopGateway } from "../claude/desktop-gateway-state";
-import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../claude/desktop-applied-marker";
-import { localClientSyncAllowed } from "../codex/desired-state";
-import { materializeRouteIntent, type RouteClientOutcome, type RouteIntentApplyInput, type RouteIntentReceipt } from "../integrations/route-intent";
-import type { HomeRouteIntentDocument, RouteClientId, RouteClientIntent } from "../link/route-intent";
-import type { OcxConfig } from "../types";
-import { assertNoClientDisconnectPending, readClientConnectionState, sameClientConnectionOwner } from "./state";
-import { withClientLifecycle, withClientLifecycleSync, type ClientLifecycleLockDeps } from "./lifecycle-lock";
-import { assertRouteIntentPort } from "./sync-runtime";
-import { resolveApiSurfaceSettings } from "../protocols/settings";
-import { nativeIntentFields, prepareNativeIntentWrite, publishNativeIntentWrite } from "./route-intent-native-originals";
-
-export interface LocalRouteIntentOptions extends Pick {
- /** Connect/sync already applied the Home's Codex settings through the existing injector. */
- codexApplied?: boolean;
- lifecycleLockDeps?: ClientLifecycleLockDeps;
- /** The native client a Home write is deciding; only it may change a Home native switch. */
- decidedClient?: RouteClientId;
-}
-
-/**
- * The Home's own native switch, read rather than decided by this apply (a sync, or a write for
- * another client). It is never rewritten here; only the client's owned artifacts follow it.
- */
-function liveHomeSwitch(document: HomeRouteIntentDocument, client: RouteClientId, options: LocalRouteIntentOptions): boolean {
- return document.sources[client] === "native" && options.decidedClient !== client && readClientConnectionState().kind !== "connected";
-}
-
-/**
- * An enabled Home Grok or Desktop switch nobody is deciding right now gets exactly the startup
- * behavior: Grok follows the startup gate and Desktop only refreshes a gateway profile OpenCodex
- * already owns. The outcome is "current": the switch is satisfied without creating anything.
- */
-function implicitHomeIntent(document: HomeRouteIntentDocument, client: RouteClientId, intent: RouteClientIntent, options: LocalRouteIntentOptions): boolean {
- return intent.enabled && liveHomeSwitch(document, client, options);
-}
-const IMPLICIT_DESKTOP_MESSAGE = "No OpenCodex-owned Claude Desktop profile to refresh; choose ocx route to claude-desktop on to create one";
-
-/**
- * First-party Desktop already routes through OpenCodex, so an enabled intent is satisfied without
- * a write. Turning it off changes the Desktop mode, which only its own transaction may do.
- */
-function firstPartyDesktopOutcome(enabled: boolean, target?: string): RouteClientOutcome {
- const base = { client: "claude-desktop" as const, ...(target === undefined ? {} : { target }) };
- if (enabled) return { ...base, outcome: "current", message: "Claude Desktop first-party mode already routes through OpenCodex" };
- const command = readClientConnectionState().kind === "connected"
- ? "switch modes with ocx claude desktop apply --gateway, then run ocx route to sync"
- : "turn it off with ocx integrations native claude-desktop off";
- return { ...base, outcome: "conflict", message: `Claude Desktop is in first-party mode; ${command}` };
-}
-
-/**
- * Caller holds the lifecycle fence; the config coordinator rechecks guard before each commit.
- * `recordStandalone` is true only for a Child's link write: it records the standalone value
- * write-ahead so disconnect or a failed enrollment can restore it. Home writes are the
- * operator's own decisions and are not recorded.
- */
-export function persistNativeRouteIntent(client: "codex" | "claude" | "claude-desktop" | "grok", enabled: boolean, guard: () => void, snapshot: OcxConfig,
- recordStandalone: boolean): OcxConfig {
- const now = new Date();
- const apply = (config: OcxConfig): boolean => {
- const previous = client === "claude" ? config.claudeCode?.enabled !== false : config.clientIntegrations?.[client] !== false;
- if (previous === enabled) return false;
- if (client === "claude") {
- // The legacy flag also supplied the shared Messages API default. A local opt-out
- // must preserve that preexisting exposure for other devices and Desktop clients.
- if (!enabled && resolveApiSurfaceSettings(config).messages.source === "claude-code-legacy") {
- config.apiSurfaces = { ...config.apiSurfaces, messages: { enabled: resolveApiSurfaceSettings(config).messages.enabled } };
- }
- commitClaudeCodeBlock(config, { ...config.claudeCode, enabled }, now);
- }
- else config.clientIntegrations = { ...config.clientIntegrations, [client]: enabled };
- return true;
- };
- const result = withConfigMutationLockSync(() => {
- let recorded = false;
- if (recordStandalone) {
- guard();
- const base = readConfigForAdmittedMutation(), planned = structuredClone(base);
- if (apply(planned)) recorded = prepareNativeIntentWrite(nativeIntentFields(base), nativeIntentFields(planned));
- }
- const outcome = mutatePersistedConfig(config => {
- guard();
- return { changed: apply(config), value: config };
- });
- if (outcome.status !== "unavailable" && recorded) publishNativeIntentWrite(nativeIntentFields(outcome.value));
- return outcome;
- });
- if (result.status === "unavailable") throw new Error(`Local native route intent could not be persisted (${result.reason})`);
- if (client === "claude") {
- snapshot.claudeCode = { ...snapshot.claudeCode, enabled,
- ...(result.value.claudeCode?.authModeMigratedAt ? { authModeMigratedAt: result.value.claudeCode.authModeMigratedAt } : {}) };
- if (result.value.apiSurfaces?.messages !== undefined) snapshot.apiSurfaces = { ...snapshot.apiSurfaces, messages: { ...result.value.apiSurfaces.messages } };
- }
- else if (result.value.clientIntegrations) snapshot.clientIntegrations = { ...result.value.clientIntegrations };
- else delete snapshot.clientIntegrations;
- return result.value;
-}
-
-async function applyNative(
- input: RouteIntentApplyInput, options: LocalRouteIntentOptions, client: "codex" | "claude" | "claude-desktop" | "grok", intent: RouteClientIntent,
-): Promise {
- const guard = input.beforeWrite!;
- const home = input.home ?? homedir();
- const env = input.env ?? process.env;
- const desktopMode = client === "claude-desktop" ? await import("../claude/desktop-first-party") : undefined;
- const homeSwitch = liveHomeSwitch(input.document, client, options);
- // The committed flag reaches the caller's working snapshot too. The management owner mirrors
- // these individual flags onto its live config; a copied request config cannot do that for it.
- const committedConfig = withClientLifecycleSync(() => withConfigMutationLockSync(() => {
- guard();
- if (desktopMode) {
- const current = loadConfig();
- if (desktopMode.resolveClaudeDesktopMode(current, desktopMode.observeClaudeDesktopMode(current)) === "first-party") return null;
- }
- if (homeSwitch) return loadConfig();
- // Only a connected Child's write displaces a standalone value; the Home owns its own flags.
- return persistNativeRouteIntent(client, intent.enabled, guard, input.config, readClientConnectionState().kind === "connected");
- }), options.lifecycleLockDeps);
- if (committedConfig === null) return firstPartyDesktopOutcome(intent.enabled);
- if (client === "codex") {
- const { getCodexHome, CODEX_CONFIG_PATH, DEFAULT_CATALOG_PATH } = await import("../codex/paths");
- const { injectCodexConfig, restoreNativeCodexAsync, currentExternalCodexModelProvider, isCodexRoutingInjected } = await import("../codex/inject");
- const target = CODEX_CONFIG_PATH;
- guard();
- if (!existsSync(getCodexHome())) return { client, target, outcome: intent.enabled ? "not_installed" : "current" };
- if (intent.enabled && options.codexApplied) return { client, target, outcome: "current" };
- const externalProvider = currentExternalCodexModelProvider();
- if (externalProvider) {
- const { approvedCodexProviders } = await import("../codex/provider-routing");
- if (!approvedCodexProviders().includes(externalProvider)) return { client, target, outcome: intent.enabled ? "conflict" : "current", message: "Codex has an external provider" };
- }
- if (!intent.enabled && !isCodexRoutingInjected()) return { client, target, outcome: "current" };
- const config = committedConfig;
- if (!intent.enabled) {
- const result = await restoreNativeCodexAsync({ withWriteGuard: work => withConfigMutationLockSync(() => { guard(); return work(); }) });
- return { client, target, outcome: result.success ? "current" : "failed", message: result.message };
- }
- const state = readClientConnectionState();
- const result = await injectCodexConfig(input.config.port, { ...config, syncResumeHistory: false }, {
- catalogPath: DEFAULT_CATALOG_PATH, beforeClientWrite: guard,
- ...(state.kind === "connected" ? { journalOwner: { kind: "client" as const, apiKeyId: state.value.apiKeyId },
- routingTarget: { baseUrl: `http://127.0.0.1:${input.config.port}/v1`, requiresAdmissionToken: false, tokenEnv: "OPENCODEX_API_AUTH_TOKEN" as const } } : {}),
- });
- return { client, target, outcome: result.success && result.status !== "skipped" ? "current" : "failed", message: result.message };
- }
- if (client === "claude") {
- const target = env.CLAUDE_CONFIG_DIR || join(home, ".claude");
- if (!existsSync(target)) return { client, target, outcome: intent.enabled ? "not_installed" : "current" };
- if (!intent.enabled) {
- const { injectClaudeAgentDefs } = await import("../claude/agents-inject");
- const cleaned = withClientLifecycleSync(() => { guard(); return injectClaudeAgentDefs(committedConfig, {}, target); }, options.lifecycleLockDeps);
- if (cleaned === null) return { client, target, outcome: "failed", message: "Owned Claude agent definitions could not be removed" };
- }
- // Claude Code uses the existing launcher and selectedClients gate; no foreign config is copied.
- return { client, target, outcome: "current" };
- }
- if (client === "grok") {
- const { injectGrokConfig, stripGrokConfig } = await import("../grok/inject");
- const { inspectGrokConfig } = await import("../grok/inspect");
- const grokHome = env.GROK_HOME || join(home, ".grok");
- const target = join(grokHome, "config.toml");
- if (implicitHomeIntent(input.document, client, intent, options) && !localClientSyncAllowed(input.config)) {
- return { client, target, outcome: "current", message: "Grok sync is gated on this Home, as it is at startup" };
- }
- return withClientLifecycleSync(() => withConfigMutationLockSync(() => {
- guard();
- const state = inspectGrokConfig({ grokHome });
- if (state.kind === "not_installed") return { client, target, outcome: intent.enabled ? "not_installed" : "current" };
- if (state.kind === "orphaned_marker") return { client, target, outcome: "conflict", message: "Grok managed block is incomplete" };
- if (!intent.enabled && state.kind === "absent") return { client, target, outcome: "current" };
- const destination = localInferenceDestination(input.config, input.config.port);
- if (intent.enabled && destination.requiresAdmissionToken) return { client, target, outcome: "failed", message: "Grok requires a credential-free local listener" };
- const result = intent.enabled ? injectGrokConfig(destination.port, input.models.map(model => ({
- id: model.namespaced, name: model.displayName, contextWindow: model.contextWindow,
- reasoningEfforts: model.reasoningEfforts, defaultReasoningEffort: model.defaultReasoningEffort,
- })), { grokHome, hostname: "127.0.0.1" }) : stripGrokConfig({ grokHome });
- return { client, target, outcome: result.ok && !result.skippedReason ? "current" : "failed", message: result.message };
- }), options.lifecycleLockDeps);
- }
- const desktop = await import("../claude/desktop-3p");
- guard();
- // The durable marker: a caller's snapshot can predate a Desktop apply by another process.
- const inspected = desktop.inspectDesktop3pConfigLibrary({ appliedFingerprint: committedConfig.claudeCode?.desktopProfile?.appliedFingerprint });
- const target = inspected.selectedProfilePath ?? inspected.libraryPath;
- if (inspected.kind === "not_installed") return { client, target, outcome: intent.enabled ? "not_installed" : "current" };
- const state = readClientConnectionState();
- // Connected profiles carry their own owner/projection evidence, not the standalone marker.
- if (!intent.enabled && state.kind === "connected") {
- guard();
- const result = desktop.removeDesktop3pStandardPivot({ lifecycleLockDeps: options.lifecycleLockDeps });
- return { client, target, outcome: result.ok ? "current" : result.reason === "conflict" ? "conflict" : "failed" };
- }
- const firstParty = (config: OcxConfig) => desktopMode!.resolveClaudeDesktopMode(config, desktopMode!.observeClaudeDesktopMode(config)) === "first-party";
- const modeConflict: RouteClientOutcome = { client, target, outcome: "conflict", message: "Claude Desktop first-party mode needs its existing mode-switch transaction" };
- if (intent.enabled && state.kind === "connected") {
- const { applyRemoteDesktopStore } = await import("../claude/desktop-remote-store");
- const { readServiceApiTokenState } = await import("../lib/service-secrets");
- const models = desktop.generateDesktop3pModels(input.models.filter(model => model.native).map(model => model.id),
- input.models.filter(model => !model.native).map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })));
- return withClientLifecycleSync(held => withConfigMutationLockSync(() => {
- guard();
- if (firstParty(loadConfig())) return firstPartyDesktopOutcome(true, target);
- const token = readServiceApiTokenState();
- if (token.kind !== "present") return { client, target, outcome: "failed", message: "Child service credential is unavailable" };
- const result = applyRemoteDesktopStore(held, {
- owner: { serverUrl: state.value.serverUrl, apiKeyId: state.value.apiKeyId, connectedAt: state.value.connectedAt },
- expectedTokenFingerprint: token.fingerprint, baseUrl: `http://127.0.0.1:${input.config.port}`,
- apiKey: "opencodex-loopback", mode: "static", models, localListener: true, overwriteConflict: options.overwriteConflict,
- });
- return { client, target: result.ok ? result.path ?? target : target, outcome: result.ok ? "current" : result.reason === "conflict" ? "conflict" : "failed" };
- }), options.lifecycleLockDeps);
- }
- if (!intent.enabled && inspected.kind === "no_owned_state") return { client, target, outcome: "current" };
- const appliedFingerprint = committedConfig.claudeCode?.desktopProfile?.appliedFingerprint;
- const refreshOnly = implicitHomeIntent(input.document, client, intent, options);
- const changedProfile = ["unsafe", "broken", "gateway_drifted"].includes(inspected.kind);
- if (changedProfile && refreshOnly) {
- // Only an OpenCodex-owned profile can have changed under a refresh. An unreadable library or one
- // selecting another app's profile was never ours, so the refresh has nothing to do there.
- const owned = inspected.ownedProfileActive === true || (inspected.ownedProfileActive === null && !!appliedFingerprint);
- if (!owned) return { client, target, outcome: "current", message: IMPLICIT_DESKTOP_MESSAGE };
- // A refresh never takes over a changed profile, even with overwrite: that is a decided write.
- return { client, target, outcome: "conflict",
- message: "Claude Desktop ownership or profile changed; replace it with ocx route to claude-desktop on --device home --overwrite-conflict" };
- }
- if (changedProfile && !options.overwriteConflict) return { client, target, outcome: "conflict", message: "Claude Desktop ownership or profile changed" };
- // Only an absent or untouched profile satisfies a refresh-only switch.
- if (refreshOnly && (inspected.kind !== "gateway_ours" || !appliedFingerprint)) return { client, target, outcome: "current", message: IMPLICIT_DESKTOP_MESSAGE };
- if (!intent.enabled) {
- guard();
- const result = desktop.removeDesktop3pStandardPivot({ appliedFingerprint: committedConfig.claudeCode?.desktopProfile?.appliedFingerprint, lifecycleLockDeps: options.lifecycleLockDeps });
- return { client, target, outcome: result.ok ? "current" : "failed" };
- }
- const { getClaudePickerController } = await import("../claude/intercept/runtime");
- const { runDesktopTransition } = await import("../claude/desktop-picker");
- // Mode switches share picker exclusion in-process and L→C across CLI/server writers.
- // Keep the file write and marker together, reusing the existing writer with this lease.
- return runDesktopTransition(getClaudePickerController(), async () => withClientLifecycleSync(held => withConfigMutationLockSync(() => {
- guard();
- const config = loadConfig();
- if (firstParty(config)) return firstPartyDesktopOutcome(true, target);
- const profile = config.claudeCode?.desktopProfile;
- const result = desktop.writeDesktop3pConfig(input.config.port,
- input.models.filter(model => model.native).map(model => model.id),
- input.models.filter(model => !model.native).map(model => ({ provider: model.provider, id: model.id, contextWindow: model.contextWindow })),
- undefined, "static", profile, undefined, options.lifecycleLockDeps,
- refreshOnly ? { appliedFingerprint: appliedFingerprint!, admit: () => !firstParty(loadConfig()) } : undefined, held);
- if (refreshOnly && result.reason === "desktop_refresh_only_skipped") return { client, target, outcome: "current", message: IMPLICIT_DESKTOP_MESSAGE };
- if (!result.written) return { client, target, outcome: "failed", message: result.reason ?? "Claude Desktop writer refused" };
- guard();
- if (firstParty(loadConfig())) return modeConflict;
- if (refreshOnly) {
- // A refresh keeps the existing mode; only the applied marker follows the new bytes.
- const marked = commitDesktopAppliedMarker(captureDesktopAppliedMarker(profile), result.fingerprint!);
- return { client, target, outcome: marked.status !== "unavailable" && marked.value ? "current" : "failed" };
- }
- const recorded = persistCommittedDesktopGateway(config, config.claudeCode?.desktopProfile, result.fingerprint, current => !firstParty(current));
- return { client, target, outcome: recorded.ok ? "current" : "failed" };
- }), options.lifecycleLockDeps), { config: committedConfig });
-}
-
-export function materializeLocalRouteIntent(
- config: OcxConfig, document: HomeRouteIntentDocument, models: readonly ExportModel[], options: LocalRouteIntentOptions = {},
-): Promise {
- const expected = readClientConnectionState();
- const configuredPort = loadConfig().port;
- const beforeWrite = () => {
- options.beforeWrite?.();
- assertRouteIntentPort(config.port);
- if (loadConfig().port !== configuredPort) throw new Error("client_runtime_changed");
- assertNoClientDisconnectPending();
- const current = readClientConnectionState();
- if (expected.kind === "connected" ? current.kind !== "connected" || !sameClientConnectionOwner(current.value, expected.value)
- : current.kind !== expected.kind || current.kind !== "disconnected") throw new Error("client_connection_changed");
- };
- const input: RouteIntentApplyInput = { config, document: structuredClone(document), models: structuredClone([...models]), ...options, beforeWrite,
- ...(expected.kind === "connected" && expected.value.link ? { linkId: expected.value.link.linkId } : {}),
- localOff: expected.kind === "connected" && expected.value.transport === "link" ? undefined : [] };
- return materializeRouteIntent({ ...input, withFileWrites: work => withClientLifecycle(work, options.lifecycleLockDeps),
- withReceiptWrite: write => withClientLifecycleSync(() => { beforeWrite(); write(); }, options.lifecycleLockDeps),
- applyNative: (client, intent) => applyNative(input, { ...options, overwriteConflict: options.overwriteConflict === true && (options.overwriteClient === undefined || options.overwriteClient === client) }, client, intent) });
-}
diff --git a/src/client/route-intent-file-originals.ts b/src/client/route-intent-file-originals.ts
deleted file mode 100644
index ee0b1c0fa9f..00000000000
--- a/src/client/route-intent-file-originals.ts
+++ /dev/null
@@ -1,97 +0,0 @@
-/**
- * File integrations a linked Child owned before linking.
- *
- * A Child applying its Home's route intent disables, refreshes or creates file-integration blocks.
- * Without this record, disconnect cannot tell a block the link created from one the person had
- * set up on their own, and removed both. At the first link apply, before any client changes, the
- * Child records which file clients had an ownership record (with their model-name policy). The
- * record is private, bound to the link that captured it, and carries no paths or secrets.
- *
- * Disconnect restores those clients to their standalone state: a client the link left owned is
- * kept as is, a client the link disabled is applied again, and a client the person changed in the
- * meantime (a local opt-out, or a block of their own) is left alone. Clients the link created keep
- * the ordinary cleanup. Aside profiles are not recorded and keep that cleanup too.
- *
- * Callers hold the client lifecycle lock; reads are strict, so a damaged record refuses the
- * disconnect instead of silently dropping a standalone integration.
- */
-import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
-import { join } from "node:path";
-import { atomicWriteFile, isMissingPathError } from "../config/atomic-write";
-import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
-import { hardenSecretDir } from "../lib/windows-secret-acl";
-import { linkDir } from "../link/paths";
-import { ROUTE_CLIENT_IDS, type RouteClientId } from "../link/route-intent";
-
-type ModelNames = "alias" | "routed";
-export interface FileIntentOriginals {
- version: 1;
- linkId: string;
- clients: Partial>;
-}
-
-const RECORD_MAX_BYTES = 16 * 1024;
-const NOT_FILE_CLIENTS: readonly RouteClientId[] = ["codex", "claude", "claude-desktop", "grok", "aside"];
-const FILE_CLIENTS = ROUTE_CLIENT_IDS.filter(client => !NOT_FILE_CLIENTS.includes(client));
-const isRecord = (value: unknown): value is Record => value !== null && typeof value === "object" && !Array.isArray(value);
-
-export function fileIntentOriginalsPath(): string { return join(linkDir(), "file-intent-originals.json"); }
-
-const unsafe = (): never => {
- throw new Error(`File integration originals record is unreadable; inspect or remove ${fileIntentOriginalsPath()} and retry`);
-};
-
-function parse(value: unknown): FileIntentOriginals {
- if (!isRecord(value) || value.version !== 1 || typeof value.linkId !== "string" || !/^lnk_[0-9a-f]{16}$/.test(value.linkId)
- || !isRecord(value.clients) || Object.keys(value).length !== 3) return unsafe();
- for (const [client, entry] of Object.entries(value.clients)) {
- if (!(FILE_CLIENTS as readonly string[]).includes(client) || !isRecord(entry)
- || Object.keys(entry).some(key => key !== "modelNames")
- || (entry.modelNames !== undefined && entry.modelNames !== "alias" && entry.modelNames !== "routed")) return unsafe();
- }
- return value as unknown as FileIntentOriginals;
-}
-
-/** Missing reads as null; a present but malformed record refuses. */
-export function readFileIntentOriginals(): FileIntentOriginals | null {
- let stat;
- try { stat = lstatSync(fileIntentOriginalsPath()); } catch (error) { if (isMissingPathError(error)) return null; throw error; }
- if (!stat.isFile() || stat.size > RECORD_MAX_BYTES) return unsafe();
- let value: unknown;
- try { value = JSON.parse(readFileSync(fileIntentOriginalsPath(), "utf8")); } catch { return unsafe(); }
- return parse(value);
-}
-
-function writeRecord(record: FileIntentOriginals): void {
- parse(record);
- const dir = linkDir();
- assertNotRealHomeUnderTest(join(dir, ".."));
- mkdirSync(dir, { recursive: true, mode: 0o700 });
- if (process.platform === "win32") hardenSecretDir(dir, { required: true });
- else chmodSync(dir, 0o700);
- atomicWriteFile(fileIntentOriginalsPath(), `${JSON.stringify(record, null, 2)}\n`);
- if (process.platform !== "win32") chmodSync(fileIntentOriginalsPath(), 0o600);
-}
-
-export function clearFileIntentOriginals(): void {
- assertNotRealHomeUnderTest(join(linkDir(), ".."));
- try { unlinkSync(fileIntentOriginalsPath()); } catch (error) { if (!isMissingPathError(error)) throw error; }
-}
-
-/**
- * Write-ahead capture for a link's first apply. `firstApply` is false once a route receipt bound
- * to a link exists, so clients a link created are never recorded as standalone. A record from
- * another link is stale and is replaced.
- */
-export function captureFileIntentOriginals(linkId: string, firstApply: boolean,
- owners: Partial>): void {
- const existing = readFileIntentOriginals();
- if (existing?.linkId === linkId || !firstApply) return;
- const clients: FileIntentOriginals["clients"] = {};
- for (const client of FILE_CLIENTS) {
- const owner = owners[client];
- if (owner) clients[client] = owner.modelNames ? { modelNames: owner.modelNames } : {};
- }
- if (Object.keys(clients).length === 0) { if (existing) clearFileIntentOriginals(); return; }
- writeRecord({ version: 1, linkId, clients });
-}
diff --git a/src/client/route-intent-native-originals.ts b/src/client/route-intent-native-originals.ts
deleted file mode 100644
index 2e73e1001d8..00000000000
--- a/src/client/route-intent-native-originals.ts
+++ /dev/null
@@ -1,201 +0,0 @@
-/**
- * Standalone native originals for a linked Child.
- *
- * A Child applying its Home's route intent writes the Child's own native switches
- * (clientIntegrations.codex|grok|claude-desktop, claudeCode.enabled, and as side effects
- * claudeCode.authModeMigratedAt and apiSurfaces.messages). This private, machine-scoped
- * record remembers, per field, the value that existed before route intent first changed it
- * (absent is a value: null), plus the last value this writer published and any value it was
- * about to publish (pending). It is captured once and never overwritten by later syncs or
- * connects, and it is not bound to a link: it means "the standalone value before route intent
- * touched it", which stays true after a crashed connect followed by a different link.
- *
- * Callers hold the client lifecycle lock; every function here takes the config mutation lock,
- * preserving the documented lifecycle -> config order. Writes are write-ahead: the original and
- * pending value reach this file before the atomic config write, and the value is marked
- * published after it. Restore (disconnect, failed enrollment) puts the original back only for a
- * field whose current value this writer published or had pending; any other value is the
- * person's own edit and is kept. The record is removed only after the restored config is read
- * back and verified, so an interrupted restore is retried safely by the next disconnect.
- *
- * Limit (ABA): a person who sets a field to exactly the value this writer published cannot be
- * told apart from the writer, and that field is restored. The Child's local opt-out list is the
- * person's own choice and is not part of this record.
- */
-import { chmodSync, lstatSync, mkdirSync, readFileSync, unlinkSync } from "node:fs";
-import { join } from "node:path";
-import { atomicWriteFile, isMissingPathError } from "../config/atomic-write";
-import { withConfigMutationLockSync } from "../config/mutation-lock";
-import { mutatePersistedConfig, readConfigForAdmittedMutation } from "../config/persisted-mutation";
-import { deleteConfigObjectChildKey } from "../config/rebase-provenance";
-import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
-import { hardenSecretDir } from "../lib/windows-secret-acl";
-import { linkDir } from "../link/paths";
-import type { OcxConfig } from "../types";
-
-export const NATIVE_INTENT_FIELDS = ["codex", "grok", "claude-desktop", "claude", "claudeMigratedAt", "messages"] as const;
-export type NativeIntentField = typeof NATIVE_INTENT_FIELDS[number];
-/** null means the key is absent; anything else is the exact persisted value. */
-export type NativeIntentSlot = boolean | string | Record | null;
-export type NativeIntentFields = Record;
-type Entry = { original: NativeIntentSlot; published?: NativeIntentSlot; pending?: NativeIntentSlot };
-export type NativeIntentOriginals = { version: 1; fields: Partial> };
-
-const RECORD_MAX_BYTES = 16 * 1024;
-const unsafe = (): never => {
- throw new Error(`Native intent originals record is unreadable; inspect or remove ${nativeIntentOriginalsPath()} and retry`);
-};
-const isRecord = (value: unknown): value is Record => value !== null && typeof value === "object" && !Array.isArray(value);
-const sameSlot = (a: unknown, b: unknown): boolean => JSON.stringify(a) === JSON.stringify(b);
-
-export function nativeIntentOriginalsPath(): string { return join(linkDir(), "native-intent-originals.json"); }
-
-const integrationField = (field: NativeIntentField): field is "codex" | "grok" | "claude-desktop" =>
- field === "codex" || field === "grok" || field === "claude-desktop";
-const parentKey = (field: NativeIntentField) => integrationField(field) ? "clientIntegrations" as const
- : field === "messages" ? "apiSurfaces" as const : "claudeCode" as const;
-const childKey = (field: NativeIntentField): string => integrationField(field) ? field
- : field === "messages" ? "messages" : field === "claude" ? "enabled" : "authModeMigratedAt";
-
-export function readNativeIntentField(config: OcxConfig, field: NativeIntentField): NativeIntentSlot {
- const parent = (config as unknown as Record)[parentKey(field)];
- if (!isRecord(parent) || !Object.hasOwn(parent, childKey(field)) || parent[childKey(field)] === undefined) return null;
- return structuredClone(parent[childKey(field)]) as NativeIntentSlot;
-}
-
-export function nativeIntentFields(config: OcxConfig): NativeIntentFields {
- return Object.fromEntries(NATIVE_INTENT_FIELDS.map(field => [field, readNativeIntentField(config, field)])) as NativeIntentFields;
-}
-
-function writeNativeIntentField(config: OcxConfig, field: NativeIntentField, value: NativeIntentSlot): void {
- const key = parentKey(field);
- if (value === null) { deleteConfigObjectChildKey(config, key, childKey(field)); return; }
- const record = config as unknown as Record;
- const parent = isRecord(record[key]) ? record[key] as Record : {};
- record[key] = { ...parent, [childKey(field)]: structuredClone(value) };
-}
-
-function validSlot(field: NativeIntentField, value: unknown): value is NativeIntentSlot {
- if (value === null) return true;
- if (field === "claudeMigratedAt") return typeof value === "string" && value.length <= 64;
- if (field === "messages") return isRecord(value) && Object.values(value).every(item => item === null || ["boolean", "string", "number"].includes(typeof item));
- return typeof value === "boolean";
-}
-
-function parse(value: unknown): NativeIntentOriginals {
- if (!isRecord(value) || value.version !== 1 || !isRecord(value.fields) || Object.keys(value).length !== 2) return unsafe();
- for (const [field, entry] of Object.entries(value.fields)) {
- if (!(NATIVE_INTENT_FIELDS as readonly string[]).includes(field) || !isRecord(entry) || !Object.hasOwn(entry, "original")) return unsafe();
- for (const [key, slot] of Object.entries(entry)) {
- if (!["original", "published", "pending"].includes(key) || !validSlot(field as NativeIntentField, slot)) return unsafe();
- }
- }
- return value as NativeIntentOriginals;
-}
-
-/** Missing reads as null. A present but unsafe or malformed record refuses rather than losing originals. */
-export function readNativeIntentOriginals(): NativeIntentOriginals | null {
- const path = nativeIntentOriginalsPath();
- let stat;
- try { stat = lstatSync(path); } catch (error) { if (isMissingPathError(error)) return null; throw error; }
- if (!stat.isFile() || stat.size > RECORD_MAX_BYTES) return unsafe();
- let value: unknown;
- try { value = JSON.parse(readFileSync(path, "utf8")); } catch { return unsafe(); }
- return parse(value);
-}
-
-function writeRecord(record: NativeIntentOriginals): void {
- parse(record);
- const path = nativeIntentOriginalsPath(), dir = linkDir();
- assertNotRealHomeUnderTest(join(dir, ".."));
- const body = `${JSON.stringify(record, null, 2)}\n`;
- if (Buffer.byteLength(body) > RECORD_MAX_BYTES) unsafe();
- mkdirSync(dir, { recursive: true, mode: 0o700 });
- if (process.platform === "win32") hardenSecretDir(dir, { required: true });
- else chmodSync(dir, 0o700);
- atomicWriteFile(path, body);
- if (process.platform !== "win32") chmodSync(path, 0o600);
-}
-
-function clearRecord(): void {
- assertNotRealHomeUnderTest(join(linkDir(), ".."));
- try { unlinkSync(nativeIntentOriginalsPath()); } catch (error) { if (!isMissingPathError(error)) throw error; }
-}
-
-/** True when the value is one this writer published or was about to publish. */
-function ownedByWriter(entry: Entry, current: NativeIntentSlot): boolean {
- return (Object.hasOwn(entry, "published") && sameSlot(entry.published, current))
- || (Object.hasOwn(entry, "pending") && sameSlot(entry.pending, current));
-}
-
-/**
- * Write-ahead half: call under the config mutation lock before the atomic config write. Only
- * fields that actually change are recorded, so a no-op toggle claims nothing. The first capture
- * of a field keeps its original forever; a pending value a crash left behind and the config
- * shows is promoted to published first.
- */
-export function prepareNativeIntentWrite(before: NativeIntentFields, after: NativeIntentFields): boolean {
- return withConfigMutationLockSync(() => {
- const changed = NATIVE_INTENT_FIELDS.filter(field => !sameSlot(before[field], after[field]));
- if (!changed.length) return false;
- const record = readNativeIntentOriginals() ?? { version: 1 as const, fields: {} };
- for (const field of changed) {
- const entry: Entry = record.fields[field] ?? { original: structuredClone(before[field]) };
- if (Object.hasOwn(entry, "pending") && sameSlot(entry.pending, before[field])) entry.published = entry.pending;
- entry.pending = structuredClone(after[field]);
- record.fields[field] = entry;
- }
- writeRecord(record);
- return true;
- });
-}
-
-/** Completion half: mark each pending value the committed config now shows as published. */
-export function publishNativeIntentWrite(actual: NativeIntentFields): void {
- withConfigMutationLockSync(() => {
- const record = readNativeIntentOriginals();
- if (!record) return;
- let changed = false;
- for (const field of NATIVE_INTENT_FIELDS) {
- const entry = record.fields[field];
- if (!entry || !Object.hasOwn(entry, "pending") || !sameSlot(entry.pending, actual[field])) continue;
- entry.published = entry.pending;
- delete entry.pending;
- changed = true;
- }
- if (changed) writeRecord(record);
- });
-}
-
-/**
- * Restore standalone values and remove the record once the restored config is verified.
- * Idempotent: a retry after an interrupted restore finds the original or the person's own
- * value and only removes the record. Returns whether a record existed.
- */
-export function restoreNativeIntentOriginals(guard: () => void): boolean {
- return withConfigMutationLockSync(() => {
- guard();
- const record = readNativeIntentOriginals();
- if (!record) return false;
- const fields = Object.entries(record.fields) as [NativeIntentField, Entry][];
- const result = mutatePersistedConfig(config => {
- guard();
- let changed = false;
- for (const [field, entry] of fields) {
- const current = readNativeIntentField(config, field);
- if (!ownedByWriter(entry, current) || sameSlot(current, entry.original)) continue;
- writeNativeIntentField(config, field, entry.original);
- changed = true;
- }
- return { changed, value: config };
- });
- if (result.status === "unavailable") throw new Error(`Standalone native intent could not be restored (${result.reason})`);
- const verified = nativeIntentFields(readConfigForAdmittedMutation());
- if (fields.some(([field, entry]) => ownedByWriter(entry, verified[field]) && !sameSlot(verified[field], entry.original))) {
- throw new Error("Standalone native intent restore was not verified; rerun the operation");
- }
- guard();
- clearRecord();
- return true;
- });
-}
diff --git a/src/client/route-intent-watch.ts b/src/client/route-intent-watch.ts
deleted file mode 100644
index e5119332bb3..00000000000
--- a/src/client/route-intent-watch.ts
+++ /dev/null
@@ -1,141 +0,0 @@
-/** Child-owned watch: authenticated reads only; local reconciliation stays with the runtime scheduler. */
-import type { OcxClientConnectionConfig } from "../types";
-import { LINK_ROUTE_INTENT_PATH } from "../link/management-routes";
-import { parseHomeRouteIntentDocument, routeIntentDigest, ROUTE_INTENT_DIGEST_PATTERN, type HomeRouteIntentDocument } from "../link/route-intent";
-import { ROUTE_INTENT_AFTER_HEADER, ROUTE_INTENT_WAKE_HEADER, ROUTE_INTENT_WATCH_HEADER } from "../link/route-intent-watch";
-import { readBoundedResponseBytes } from "../lib/bounded-body";
-import { signalWithTimeout } from "../lib/abort";
-import { fetchBoundLinkRelay } from "./link-relay-transport";
-
-const WATCH_READ_TIMEOUT_MS = 40_000; // Home wait (30 s), relay proof and bounded body share one budget.
-const WATCH_MAX_BYTES = 64 * 1024;
-const WATCH_MAX_HEADER_BYTES = 8 * 1024;
-const WATCH_RETRY_MS = 5_000;
-export type RouteIntentWatchKey = string | (() => string | null);
-export interface RouteIntentWatchReadDeps {
- /** Trusted complete-send test seam; production always uses the connection-bound relay. */
- fetchImpl?: typeof fetch;
- signal?: AbortSignal;
- timeoutMs?: number;
-}
-export type RouteIntentWatchResult = { kind: "fresh"; document: HomeRouteIntentDocument; watchSupported: boolean; wakeReason?: "changed" | "notified" | "timeout" }
- | { kind: "unsupported" } | { kind: "cancelled" } | { kind: "failed"; reason: "route_intent_watch_unavailable" };
-
-function cancel(response: Response): void {
- try { void response.body?.cancel().catch(() => {}); } catch { /* no untrusted cleanup can extend the deadline */ }
-}
-/** No selectors, redirects, management writes, cached fallback or durable receipt writes. */
-export async function watchHomeRouteIntent(
- connection: OcxClientConnectionConfig, key: RouteIntentWatchKey, afterDigest?: string, deps: RouteIntentWatchReadDeps = {},
-): Promise {
- const failed = (): RouteIntentWatchResult => ({ kind: "failed", reason: "route_intent_watch_unavailable" });
- if (deps.signal?.aborted) return { kind: "cancelled" };
- if (connection.transport !== "link" || !connection.link || !connection.apiKeyId
- || (afterDigest !== undefined && !ROUTE_INTENT_DIGEST_PATTERN.test(afterDigest))) return failed();
- const budget = deps.timeoutMs ?? WATCH_READ_TIMEOUT_MS;
- if (!Number.isInteger(budget) || budget < 1 || budget > WATCH_READ_TIMEOUT_MS) return failed();
- const deadline = signalWithTimeout(budget, deps.signal);
- let response: Response | undefined;
- try {
- // A long-lived runtime survives key rotation. Never retain the resolved credential across GETs.
- const admissionKey = typeof key === "function" ? key() : key;
- if (!admissionKey) return failed();
- const target = { tunnelPort: connection.link.tunnelPort, linkId: connection.link.linkId, apiKeyId: connection.apiKeyId, admissionKey };
- const destination = `http://127.0.0.1:${target.tunnelPort}${LINK_ROUTE_INTENT_PATH}`;
- const init: RequestInit = { method: "GET", redirect: "manual", signal: deadline.signal,
- headers: { accept: "application/json", authorization: `Bearer ${admissionKey}`, [ROUTE_INTENT_WATCH_HEADER]: "1",
- ...(afterDigest === undefined ? {} : { [ROUTE_INTENT_AFTER_HEADER]: afterDigest }) } };
- response = deps.fetchImpl ? await deps.fetchImpl(destination, init) : await fetchBoundLinkRelay(target, destination, init);
- if (response.status === 404) return { kind: "unsupported" };
- if (response.status !== 200) return failed();
- let headerBytes = 0;
- for (const [name, value] of response.headers) headerBytes += Buffer.byteLength(name) + Buffer.byteLength(value) + 4;
- const support = response.headers.get(ROUTE_INTENT_WATCH_HEADER);
- const wakeReason = response.headers.get(ROUTE_INTENT_WAKE_HEADER);
- const declared = response.headers.get("content-length");
- if (headerBytes > WATCH_MAX_HEADER_BYTES || (support !== null && support !== "1")
- || (wakeReason !== null && wakeReason !== "changed" && wakeReason !== "notified" && wakeReason !== "timeout")
- || response.headers.get("content-type")?.split(";", 1)[0]?.trim().toLowerCase() !== "application/json"
- || (declared !== null && (!/^\d+$/.test(declared) || Number(declared) > WATCH_MAX_BYTES))) return failed();
- const read = await readBoundedResponseBytes(response, { maxBytes: WATCH_MAX_BYTES, signal: deadline.signal });
- if (read.oversized) return failed();
- const document = parseHomeRouteIntentDocument(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(read.bytes)));
- return document ? { kind: "fresh", document, watchSupported: support === "1", ...(wakeReason === null ? {} : { wakeReason }) } : failed();
- } catch { return deps.signal?.aborted ? { kind: "cancelled" } : failed(); }
- finally { if (response) cancel(response); deadline.cleanup(); }
-}
-
-export interface RouteIntentRuntimeWatchDeps extends Omit {
- /** Share this with the periodic tick. Resolve after apply/report; reject busy/failure to retain the event. */
- requestSync: () => Promise;
- setTimer?: (callback: () => void, ms: number) => unknown;
- clearTimer?: (timer: unknown) => void;
-}
-export interface RouteIntentRuntimeWatch { stop(): void }
-const liveWatches = new Map();
-
-/** One GET per live link; one in-flight scheduler request plus one coalesced pending event. */
-export function startRouteIntentWatch(
- connection: OcxClientConnectionConfig, key: RouteIntentWatchKey, deps: RouteIntentRuntimeWatchDeps,
-): RouteIntentRuntimeWatch {
- if (connection.transport !== "link" || !connection.link) return { stop() {} };
- const linkId = connection.link.linkId;
- const existing = liveWatches.get(linkId);
- if (existing) return existing;
- // Do not let caller mutation redirect a long-lived watch onto a different connection.
- const owner = structuredClone(connection);
- const abort = new AbortController();
- const set = deps.setTimer ?? ((cb, ms) => { const timer = setTimeout(cb, ms); timer.unref(); return timer; });
- const clear = deps.clearTimer ?? (timer => clearTimeout(timer as ReturnType));
- let timer: unknown, retry: unknown;
- let stopped = false, syncing = false, pending = false;
- let after: string | undefined;
- const drain = async () => {
- if (stopped || syncing) return;
- syncing = true;
- try {
- while (pending && !stopped) {
- pending = false;
- try { await deps.requestSync(); }
- catch {
- pending = true;
- if (!stopped) retry = set(() => { retry = undefined; void drain(); }, WATCH_RETRY_MS);
- break;
- }
- }
- } finally { syncing = false; }
- };
- const request = () => {
- pending = true;
- if (retry === undefined) void drain();
- };
- const schedule = (ms: number) => { if (!stopped) timer = set(() => { timer = undefined; void poll(); }, ms); };
- const poll = async () => {
- if (stopped) return;
- const started = Date.now();
- const result = await watchHomeRouteIntent(owner, key, after, { ...deps, signal: abort.signal });
- if (stopped || result.kind === "cancelled") return;
- // A pre-watch Home answers immediately forever. Its existing periodic sync remains authoritative.
- if (result.kind === "unsupported" || (result.kind === "fresh" && !result.watchSupported)) return;
- if (result.kind === "failed") { schedule(WATCH_RETRY_MS); return; }
- const next = routeIntentDigest(result.document);
- const unchanged = after === next;
- after = next;
- if (!unchanged || result.wakeReason !== "timeout") request();
- // Broken peers advertising support but never waiting cannot make a tight loop.
- schedule(unchanged ? Math.max(0, 1_000 - (Date.now() - started)) : 0);
- };
- const handle: RouteIntentRuntimeWatch = { stop() {
- if (stopped) return;
- stopped = true; pending = false;
- abort.abort();
- if (timer !== undefined) clear(timer);
- if (retry !== undefined) clear(retry);
- timer = retry = undefined;
- liveWatches.delete(linkId);
- // An already-entered local apply is owned/fenced by requestSync, never cancelled mid-write here.
- } };
- liveWatches.set(linkId, handle);
- schedule(0);
- return handle;
-}
diff --git a/src/client/runtime.ts b/src/client/runtime.ts
index 1d1044f51ed..75e3462f147 100644
--- a/src/client/runtime.ts
+++ b/src/client/runtime.ts
@@ -34,15 +34,11 @@ import { clientRestartRollbackAction, clientRestartSupervisor, createClientResta
import { createLinkParityReporter, type LinkParityReporter } from "./link-parity-reporter";
import { restoreLinkClientRouting } from "./stop-routing";
import { markClientRuntimeStopping, markClientRuntimeStoppingWithRetry } from "./sync-runtime";
-import { startRouteIntentTick, type RouteIntentTick } from "../integrations/route-intent-tick";
-import { startRouteIntentWatch, type RouteIntentRuntimeWatch } from "./route-intent-watch";
let activeServer: Server | null = null;
let activePort: number | null = null;
let activeSupervisor: ClientLinkSupervisor | null = null;
let activeReporter: LinkParityReporter | null = null;
-let activeRouteWatch: RouteIntentRuntimeWatch | null = null;
-let activeRouteSync: RouteIntentTick | null = null;
let recycleScheduled = false;
let recycleTimer: ReturnType | undefined;
let recycleFlight: Promise | null = null;
@@ -171,9 +167,6 @@ export async function recycleStandalone(
const replacementEnv = port && process.env.OCX_SERVICE !== "1"
? withSiblingMarker(standaloneRecycleEnv(process.env, disconnectedTokenFingerprint), issueSiblingHandoff)
: null;
- activeReporter?.stop(); activeReporter = null;
- activeRouteWatch?.stop(); activeRouteWatch = null;
- activeRouteSync?.stop(); activeRouteSync = null;
try {
await activeSupervisor?.stop();
} catch (error) {
@@ -384,9 +377,6 @@ export async function startClientRuntime(
if (linkMode && state.value.link && linkKey) {
activeReporter = createLinkParityReporter({ connection: state.value, linkKey, runtimeStartedAt: CLIENT_RUNTIME_STARTED_AT });
activeReporter.start();
- const scheduler = startRouteIntentTick(state.value, { onApplied: () => activeReporter?.requestReport() });
- activeRouteSync = scheduler;
- activeRouteWatch = startRouteIntentWatch(state.value, linkKey, { requestSync: () => scheduler.requestSync() });
}
};
if (!rollback) {
@@ -418,8 +408,6 @@ export async function startClientRuntime(
credentialObservation = null;
activeReporter?.stop();
activeReporter = null;
- activeRouteWatch?.stop(); activeRouteWatch = null;
- activeRouteSync?.stop(); activeRouteSync = null;
try {
await activeSupervisor?.stop();
} catch (error) {
@@ -499,8 +487,6 @@ export async function startClientRuntime(
credentialObservation = null;
activeReporter?.stop();
activeReporter = null;
- activeRouteWatch?.stop(); activeRouteWatch = null;
- activeRouteSync?.stop(); activeRouteSync = null;
try {
await activeSupervisor?.stop();
} catch (error) {
diff --git a/src/client/sync-runtime.ts b/src/client/sync-runtime.ts
index c11d93824fe..883b013b6e9 100644
--- a/src/client/sync-runtime.ts
+++ b/src/client/sync-runtime.ts
@@ -35,13 +35,6 @@ export function clientSyncRuntimeGuard(): () => void {
};
}
-/** Offline enrollment keeps the configured port. A published runtime must agree before apply. */
-export function assertRouteIntentPort(port: number): void {
- const runtime = readSyncRuntime();
- if (runtime?.clientStopping) throw new ClientSyncRuntimeError("client_runtime_stopping");
- if (runtime && runtime.port !== port) throw new ClientSyncRuntimeError("client_runtime_changed");
-}
-
/** The same C transaction excludes every sync commit; never mark a replacement runtime. */
export function markClientRuntimeStopping(expected: Required>): void {
withConfigMutationLockSync(() => {
diff --git a/src/codex/catalog/bundled.ts b/src/codex/catalog/bundled.ts
index 09178baa28e..8b2e88613d7 100644
--- a/src/codex/catalog/bundled.ts
+++ b/src/codex/catalog/bundled.ts
@@ -38,7 +38,6 @@ import { retainedNativeInheritanceTemplate } from "./native-template";
import type { RawCatalog, RawEntry } from "./parsing";
import { isSpawnableCodexCandidate } from "../exec-invocation";
import { validatedCodexExecInvocation, validatedCodexExecInvocationAsync } from "../exec-boundary";
-import { macCodexAppCommands } from "../mac-app-bundle";
import { isPlainObject } from "../../lib/plain-data";
import {
parsePersistedCodexRuntime,
@@ -52,7 +51,6 @@ import {
import type {
DeepReadonly,
EffortClampDiagnostic,
- ResolveCodexRuntimeResult,
ResolvedCodexRuntime,
} from "../runtime";
import type {
@@ -334,37 +332,6 @@ function bundledRuntimeDeps(deps: BundledCatalogDeps) {
};
}
-/**
- * Candidates for the bundled-catalog probe.
- *
- * When discovery finds no Codex CLI it answers with the bare `codex` sentinel, so the probe
- * asks a command that does not exist while a real CLI sits inside ChatGPT.app. Discovery only
- * offers that app candidate after classifying the bundle's XML `Info.plist` identity, so a
- * bundle it cannot classify (a binary plist, or an identity assertion that moved with an app
- * update) left `ocx connect status` reporting `unverified` on a macOS Child whose only Codex
- * was the bundled CLI (#8360).
- *
- * The known app locations are therefore added after a bare sentinel. This list grants no
- * trust: every execution still passes the app-bundle signature and ownership gate in
- * `exec-boundary.ts`, so an unverifiable bundle yields `null` rather than an invented ladder.
- * Windows needs no entry here because its Codex App install root under LOCALAPPDATA is already
- * enumerated by `installedCodexCandidates` in `runtime.ts`.
- */
-function bundledCatalogCandidates(
- deps: BundledCatalogDeps,
- resolved: ResolveCodexRuntimeResult | null,
-): string[] {
- const candidates = deps.commandCandidates?.() ?? [resolved!.runtime.command];
- // An explicit empty list asks for no probe at all.
- if (candidates.length === 0) return candidates;
- if ((deps.platform ?? process.platform) !== "darwin") return candidates;
- // Only the unresolved sentinel is a bare name; a PATH or installed selection is absolute, and
- // that selection is the runtime the operator is already running, not a gap to fill.
- if (candidates.some(command => command.includes("/") || command.includes("\\"))) return candidates;
- const exists = deps.existsSync ?? existsSync;
- return [...candidates, ...macCodexAppCommands(deps).filter(command => exists(command))];
-}
-
function* bundledCatalogSteps(
candidates: string[],
cacheKey: string | null,
@@ -423,7 +390,7 @@ export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): Readonly
const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.execFile && !deps.configDir && !deps.env;
const resolved = deps.commandCandidates ? null : resolveAndPersistCodexRuntime(bundledRuntimeDeps(deps));
const inputs = codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps));
- const steps = bundledCatalogSteps(bundledCatalogCandidates(deps, resolved),
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
useCache ? bundledRuntimeKey(resolved!.runtime) : null,
() => inputs === codexRuntimeSelectionIdentity(bundledRuntimeDeps(deps)));
let step = steps.next();
@@ -482,7 +449,7 @@ export function loadBundledCodexCatalogAsync(
const runtimeEpoch = codexRuntimeStateEpoch();
const current = () => active() && runtimeEpoch === codexRuntimeStateEpoch()
&& flight.inputs === codexRuntimeSelectionIdentity(runtimeDeps);
- const steps = bundledCatalogSteps(bundledCatalogCandidates(deps, resolved),
+ const steps = bundledCatalogSteps(deps.commandCandidates?.() ?? [resolved!.runtime.command],
useCache ? bundledRuntimeKey(resolved!.runtime) : null, current, false);
let step = steps.next();
while (!step.done) {
diff --git a/src/codex/journal-config-restore.ts b/src/codex/journal-config-restore.ts
deleted file mode 100644
index 5f928131ee0..00000000000
--- a/src/codex/journal-config-restore.ts
+++ /dev/null
@@ -1,217 +0,0 @@
-import { isDeepStrictEqual } from "node:util";
-import { chmodSync, closeSync, fsyncSync, mkdtempSync, openSync, writeFileSync } from "node:fs";
-import { join } from "node:path";
-import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
-import { isOcxRoutingMarkerLine } from "./injected-marker";
-import { transformManagedSubagentDefaults, type WrittenSubagentDefaults } from "./subagent-defaults";
-import { rootAssignmentKey, rootSourceLines, sourceAssignment, sourceText } from "./toml-source-lines";
-
-const ROOT_KEYS = new Set(["openai_base_url", "experimental_realtime_ws_base_url", "model_catalog_json", "model_provider", "web_search"]);
-type Field = { start: number; end: number; text: string; value: unknown };
-export type JournalConfigFields = Record;
-
-/** Only injection-owned fields are captured; hooks, projects and paths never enter this map. */
-function fields(content: string): Map {
- const { lines, rootEnd } = rootSourceLines(content);
- // Inline/dotted tables have no independently replaceable owned block. Detect
- // them before restoring any root key; leaving them behind would lose recovery
- // evidence while the injected provider still exists. Decode key paths as TOML,
- // including quoted keys, without parsing unrelated (possibly wide) integers.
- let header = "";
- const keyToken = String.raw`(?:[A-Za-z0-9_-]+|"(?:[^"\\]|\\.)*"|'[^']*')`;
- const assignment = new RegExp(`^\\s*(${keyToken}(?:\\s*\\.\\s*${keyToken})*)\\s*=`);
- for (let at = 0; at < lines.length; at++) {
- const line = lines[at]!;
- if (!line.structural) continue;
- if (/^\s*\[/.test(line.text)) { header = line.text; continue; }
- const prefix = assignment.exec(line.text)?.[1];
- if (!prefix) continue;
- const context = Bun.TOML.parse(header) as Record;
- // Explicit owned blocks are handled losslessly by the block scan below.
- if (["model_providers", "profiles"].some(key => {
- const table = context[key];
- return table && typeof table === "object" && Object.hasOwn(table, "opencodex");
- })) continue;
- const shape = Bun.TOML.parse(`${header}\n${prefix} = {}`) as Record;
- for (const key of ["model_providers", "profiles"]) {
- const table = shape[key];
- if (!table || typeof table !== "object") continue;
- if (Object.hasOwn(table, "opencodex")) throw new Error("unsupported dotted owned Codex table");
- if (Object.keys(table).length) continue; // an unrelated provider/profile path
- let end = at + 1;
- while (end < lines.length && !lines[end]!.structural) end++;
- const value = Bun.TOML.parse(`${header}\n${sourceText(lines.slice(at, end))}`) as Record;
- const actual = value[key];
- if (actual && typeof actual === "object" && Object.hasOwn(actual, "opencodex")) {
- throw new Error("unsupported inline or dotted owned Codex table");
- }
- }
- }
- const found = new Map();
- const put = (key: string, value: Field) => {
- if (found.has(key)) throw new Error("duplicate owned Codex field");
- found.set(key, value);
- };
- for (let at = 0; at < rootEnd; at++) {
- if (!lines[at]!.structural) continue;
- const key = rootAssignmentKey(lines[at]!.text);
- if (!key || !ROOT_KEYS.has(key)) continue;
- const span = sourceAssignment(lines, at, rootEnd);
- if (!span) throw new Error("invalid owned Codex field");
- put(key, span);
- }
- for (let at = rootEnd; at < lines.length; at++) {
- const line = lines[at]!;
- if (!line.structural || !/^\s*\[/.test(line.text)) continue;
- const header = Bun.TOML.parse(line.text) as Record;
- const target = ["model_providers", "profiles"].find(key => {
- const table = header[key];
- return table && typeof table === "object" && Object.hasOwn(table, "opencodex");
- });
- let end = at + 1;
- while (end < lines.length && !(lines[end]!.structural && /^\s*\[/.test(lines[end]!.text))) end++;
- // A marker adjacent to the next header belongs to that next block, never this one.
- if (end < lines.length && lines[end - 1]?.structural && isOcxRoutingMarkerLine(lines[end - 1]!.text)) end--;
- if (target) {
- const table = (header[target] as Record).opencodex;
- if (!table || typeof table !== "object" || Object.keys(table).length) throw new Error("unsupported owned child table");
- const text = sourceText(lines.slice(at, end));
- // Child tables are refused rather than accidentally treating them as the parent block.
- const value = Bun.TOML.parse(text);
- put(`${target}.opencodex`, { start: at, end, text, value });
- }
- at = end - 1;
- }
- return found;
-}
-
-export function captureJournalConfigFields(content: string, ownership: Ownership): JournalConfigFields {
- const captured: JournalConfigFields = {};
- for (const [key, span] of fields(content)) {
- const recorded = recordedValues(ownership).get(key);
- if (isDeepStrictEqual(span.value, recorded) || key === "model_provider" && span.value === "opencodex"
- || key === "model_providers.opencodex" || key === "profiles.opencodex") captured[key] = span.text;
- }
- // Injection removes the root selection and old inline OCX profiles in URL-only mode.
- for (const key of ["model_provider", "model_providers.opencodex", "profiles.opencodex"]) captured[key] ??= null;
- return captured;
-}
-
-interface Ownership {
- injectedConfigFields?: JournalConfigFields;
- injectedOpenaiBaseUrl?: string | null;
- injectedRealtimeWsBaseUrl?: string | null;
- injectedCatalogPath?: string | null;
- injectedRootWebSearch?: string | null;
- replacedRootWebSearch?: string | null;
- injectedSubagentDefaults?: WrittenSubagentDefaults | null;
-}
-
-function recordedValues(journal: Ownership): Map {
- const values = new Map();
- for (const [key, value] of [
- ["openai_base_url", journal.injectedOpenaiBaseUrl],
- ["experimental_realtime_ws_base_url", journal.injectedRealtimeWsBaseUrl],
- ["model_catalog_json", journal.injectedCatalogPath],
- ["web_search", journal.injectedRootWebSearch],
- ] as const) if (typeof value === "string") values.set(key, value);
- return values;
-}
-
-/** Three-way restore: an edited preference stays; an edited routing field blocks hand-back. */
-export function restoreJournalConfigFields(current: string, original: string, journal: Ownership, recoverOwnedEdits = false): string | null {
- try {
- const defaults = transformManagedSubagentDefaults(current, null, journal.injectedSubagentDefaults);
- if (!defaults.ok) return null;
- current = defaults.content;
- const now = fields(current), before = fields(original);
- if (journal.replacedRootWebSearch) {
- const replaced = fields(journal.replacedRootWebSearch).get("web_search");
- if (!replaced) return null;
- before.set("web_search", replaced);
- }
- const expected = new Map();
- if (journal.injectedConfigFields) {
- for (const [key, text] of Object.entries(journal.injectedConfigFields)) {
- if (text === null) { expected.set(key, undefined); continue; }
- const parsed = fields(text);
- const field = parsed.get(key);
- if (!field) return null;
- expected.set(key, field.value);
- }
- } else {
- // Version-1 journals predate field snapshots. Only their explicit value evidence
- // authorizes removal; a whole-file hash alone never authorizes a guessed URL.
- for (const [key, value] of recordedValues(journal)) expected.set(key, value);
- if (now.get("model_provider")?.value === "opencodex") expected.set("model_provider", "opencodex");
- }
- const { bom, lines } = rootSourceLines(current);
- if (recoverOwnedEdits) {
- // Legacy journals lack field snapshots. An exact structural marker is evidence
- // only for its adjacent owned span; loopback-looking values alone prove nothing.
- for (const [key, span] of now) {
- const marker = lines[span.start - 1];
- if (key !== "web_search" && !expected.has(key) && marker?.structural && isOcxRoutingMarkerLine(marker.text)) expected.set(key, span.value);
- }
- }
- const replacements = new Map();
- const prepend: string[] = [];
- const append: string[] = [];
- const eol = lines.find(line => line.eol)?.eol || "\n";
- for (const key of new Set([...now.keys(), ...expected.keys()])) {
- const present = now.get(key), prior = before.get(key);
- if (isDeepStrictEqual(present?.value, prior?.value)) continue;
- if (!expected.has(key)) {
- if (key === "model" || key.startsWith("agents.") || key === "web_search") continue;
- return null;
- }
- const injected = expected.get(key);
- if (isDeepStrictEqual(injected, prior?.value)) continue; // never owned
- if (!isDeepStrictEqual(present?.value, injected)) {
- if (key === "model" || key.startsWith("agents.") || key === "web_search") continue;
- if (!recoverOwnedEdits) return null;
- }
- const replacement = prior ? prior.text.replace(/\r?\n/g, eol).replace(/(?:\r?\n)?$/, eol) : "";
- if (present) {
- let start = present.start;
- if (start > 0 && lines[start - 1]!.structural && isOcxRoutingMarkerLine(lines[start - 1]!.text)) start--;
- replacements.set(start, { end: present.end, text: replacement });
- } else if (replacement) {
- // A removed root value can safely return before the first table. Missing
- // non-root spans require a containing table, so refuse ambiguous placement.
- if (key.startsWith("agents.")) return null;
- if (key.includes(".")) append.push(replacement);
- else prepend.push(replacement);
- }
- }
- let out = bom + prepend.join("");
- for (let at = 0; at < lines.length;) {
- const replace = replacements.get(at);
- if (replace) { out += replace.text; at = replace.end; }
- else { out += sourceText([lines[at]!]); at++; }
- }
- return append.length ? `${out}${out.endsWith("\n") ? "" : eol}${append.join("")}` : out;
- } catch { return null; }
-}
-
-/** Private, exclusive, durable preimage. Never registered as an uninstall-owned artifact. */
-export function snapshotDisconnectRecovery(home: string, config: string | null, profile: string | null, journal: unknown): string {
- const dir = mkdtempSync(join(home, ".ocx-disconnect-recovery-"));
- chmodSync(dir, 0o700);
- hardenSecretDir(dir, { required: true });
- const path = join(dir, "snapshot.json");
- const fd = openSync(path, "wx", 0o600);
- try {
- // Harden the empty file before placing possibly sensitive profile/config bytes in it.
- hardenSecretPath(path, { required: true });
- writeFileSync(fd, JSON.stringify({ version: 1, config, profile, journal }));
- fsyncSync(fd);
- } finally { closeSync(fd); }
- if (process.platform !== "win32") {
- for (const entry of [dir, home]) {
- const directory = openSync(entry, "r");
- try { fsyncSync(directory); } finally { closeSync(directory); }
- }
- }
- return dir;
-}
diff --git a/src/codex/journal.ts b/src/codex/journal.ts
index 0836cbca561..fb8311a551e 100644
--- a/src/codex/journal.ts
+++ b/src/codex/journal.ts
@@ -10,7 +10,6 @@ import { configWriteLockFailureMessage, withConfigWriteLockHeld } from "./config
import type { LockHandle } from "./config-write-lock";
import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
import { transformManagedSubagentDefaults, type WrittenSubagentDefaults } from "./subagent-defaults";
-import { captureJournalConfigFields, restoreJournalConfigFields, snapshotDisconnectRecovery, type JournalConfigFields } from "./journal-config-restore";
/**
* Exported so that anything reasoning ABOUT the journal points at the journal.
@@ -32,7 +31,6 @@ interface Journal {
originalConfig: string;
originalProfile: string | null;
injectedConfigHash?: string;
- injectedConfigFields?: JournalConfigFields;
injectedProfileHash?: string | null;
/**
* The exact root `openai_base_url` this injection wrote, when it wrote one.
@@ -96,7 +94,6 @@ interface Journal {
}
export interface RestoreJournalResult {
- recoveryBackupDir?: string;
/** The shared config write lock was held by another opencodex writer; restore did not run. */
lockBusy?: true;
lockUnsafe?: string;
@@ -250,7 +247,6 @@ export function markJournalInjectedState(
// hashing those newer bytes and restoring the first snapshot would delete those edits.
// Keep the first hash so changed/reinjected configs take the owned-field fallback path.
journal.injectedConfigHash ??= sha256(config) ?? undefined;
- journal.injectedConfigFields = captureJournalConfigFields(config, ownership);
// The profile file is wholly generated by OpenCodex, so its latest exact hash remains safe
// to refresh and lets restore remove the latest generated profile after a port change.
journal.injectedProfileHash = sha256(profile);
@@ -358,7 +354,7 @@ export function journalOwner(options: { readOnly?: boolean } = {}): JournalOwner
}
export function restoreJournalState(
- options: { heldConfigWriteLock?: LockHandle; preserveConfigEdits?: boolean; recoverConfigForClient?: string; beforeWrite?: () => void } = {},
+ options: { heldConfigWriteLock?: LockHandle } = {},
): RestoreJournalResult {
if (codexHomeIsAbsent(CODEX_HOME)) return {
configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
@@ -372,7 +368,6 @@ export function restoreJournalState(
configChanged: false, profileChanged: false, complete: false, unverified: true, ownershipRefusal: error.reason };
}
beginCodexWriteSection(held);
- options.beforeWrite?.();
const journal = readJournal();
if (!journal) {
return { configRestored: false, profileRestored: false, configRewritten: false, profileRewritten: false,
@@ -382,12 +377,9 @@ export function restoreJournalState(
const currentProfile = existsSync(CODEX_PROFILE_PATH) ? readFileSync(CODEX_PROFILE_PATH, "utf-8") : null;
const comparison = compareJournalState(journal, currentConfig, currentProfile);
const { configUnchanged, profileUnchanged } = comparison;
- const recovery = options.recoverConfigForClient !== undefined;
- if (recovery && (!options.recoverConfigForClient || journal.owner?.kind !== "client"
- || journal.owner.apiKeyId !== options.recoverConfigForClient)) throw new Error("disconnect recovery refused: Codex journal must belong to the connected client key");
// A legacy record or interruption before markJournalInjectedState is not proof that
// later bytes belong to OpenCodex. Keep the whole pair and its recovery evidence intact.
- if ((!recovery && comparison.unverified) || (recovery && !profileUnchanged)) {
+ if (comparison.unverified) {
return {
configRestored: comparison.configAlreadyOriginal,
profileRestored: comparison.profileAlreadyOriginal,
@@ -396,18 +388,10 @@ export function restoreJournalState(
configChanged: !configUnchanged,
profileChanged: !profileUnchanged,
complete: false,
- unverified: comparison.unverified,
+ unverified: true,
};
}
- const fieldRestore = (recovery || options.preserveConfigEdits && !configUnchanged) && currentConfig !== null
- ? restoreJournalConfigFields(currentConfig, comparison.originalConfig, journal, recovery) : null;
- if ((recovery || options.preserveConfigEdits && !configUnchanged) && fieldRestore === null) {
- return { configRestored: false, profileRestored: comparison.profileAlreadyOriginal,
- configRewritten: false, profileRewritten: false, configChanged: true,
- profileChanged: !profileUnchanged, complete: false, unverified: false };
- }
-
// A matching byte hash does not authorize following a newly substituted profile link.
// Refuse the complete pair before restoring config or deleting its recovery journal.
let profileIsSymlink = false;
@@ -424,16 +408,12 @@ export function restoreJournalState(
};
}
assertCodexHomeOwner(CODEX_HOME);
- options.beforeWrite?.();
- const recoveryBackupDir = recovery
- ? snapshotDisconnectRecovery(CODEX_HOME, currentConfig, currentProfile, journal) : undefined;
let configRestored = comparison.configAlreadyOriginal;
let profileRestored = comparison.profileAlreadyOriginal;
let configRewritten = false;
let profileRewritten = false;
- if ((configUnchanged || fieldRestore !== null) && !configRestored) {
- options.beforeWrite?.();
- publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, fieldRestore ?? comparison.originalConfig, undefined, hooks));
+ if (configUnchanged && !configRestored) {
+ publishCodexArtifact(CODEX_CONFIG_PATH, held, (path, hooks) => atomicWriteFile(path, comparison.originalConfig, undefined, hooks));
configRestored = true;
configRewritten = true;
}
@@ -465,7 +445,6 @@ export function restoreJournalState(
catch { complete = false; }
}
return {
- ...(recoveryBackupDir ? { recoveryBackupDir } : {}),
configRestored,
profileRestored,
configRewritten,
diff --git a/src/codex/shim-restore-lock.ts b/src/codex/shim-restore-lock.ts
index 1de3f257519..27d1eae8a78 100644
--- a/src/codex/shim-restore-lock.ts
+++ b/src/codex/shim-restore-lock.ts
@@ -32,107 +32,70 @@ interface ShimRestoreLockRecord {
createdAt: number;
}
-interface ShimRestoreLockOwner {
+interface ShimRestoreLockSnapshot {
record: ShimRestoreLockRecord;
ownerPath: string;
- fingerprint: ShimPathFingerprint;
-}
-
-interface ShimRestoreLockSnapshot {
lockIdentity: FileIdentity;
- /** Sorted by owner filename so two snapshots compare entry by entry. */
- owners: ShimRestoreLockOwner[];
+ fingerprint: ShimPathFingerprint;
}
function restoreLockPath(): string {
return join(getConfigDir(), "codex-shim.autorestore.lock");
}
-function readShimRestoreLockOwner(path: string, entry: string): ShimRestoreLockOwner | null {
- if (!entry.endsWith(".json")) return null;
- const ownerPath = join(path, entry);
- const probe = stableShimPathProbe(ownerPath);
- if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
- try {
- const value = JSON.parse(probe.prefix) as Partial;
- if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
- || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
- || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
- if (entry !== `${value.token}.json`) return null;
- return { record: value as ShimRestoreLockRecord, ownerPath, fingerprint: probe.fingerprint };
- } catch {
- return null;
- }
-}
-
-/**
- * Every entry must be a valid token-named owner record; one unaccounted entry (foreign file,
- * unparsable or empty record, mismatched name) makes the whole lock unreadable. A normal lock
- * holds one owner. Two or more are not produced by the current acquire path, but have been
- * observed in the field (#8317) and must stay recoverable rather than block repair forever.
- */
function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
let lockIdentity: BigIntStats;
let entries: string[];
try {
lockIdentity = lstatSync(path, { bigint: true });
if (!lockIdentity.isDirectory()) return null;
- entries = readdirSync(path).sort();
+ entries = readdirSync(path);
} catch {
return null;
}
- if (entries.length === 0) return null;
- const owners: ShimRestoreLockOwner[] = [];
- for (const entry of entries) {
- const owner = readShimRestoreLockOwner(path, entry);
- if (!owner) return null;
- owners.push(owner);
- }
+ if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
+ const ownerPath = join(path, entries[0]);
+ const probe = stableShimPathProbe(ownerPath);
+ if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
try {
+ const value = JSON.parse(probe.prefix) as Partial;
+ if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
+ || typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
+ || typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
+ if (entries[0] !== `${value.token}.json`) return null;
const currentLockIdentity = lstatSync(path, { bigint: true });
if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
+ return {
+ record: value as ShimRestoreLockRecord,
+ ownerPath,
+ lockIdentity,
+ fingerprint: probe.fingerprint,
+ };
} catch {
return null;
}
- return { lockIdentity, owners };
-}
-
-/** Release only ever removes a lock it solely owns; an ambiguous set is left to stale recovery. */
-function soleShimRestoreLockOwner(path: string): ShimRestoreLockOwner | null {
- const snapshot = readShimRestoreLockSnapshot(path);
- return snapshot?.owners.length === 1 ? snapshot.owners[0] : null;
}
function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
- return sameFileIdentity(left.lockIdentity, right.lockIdentity)
- && left.owners.length === right.owners.length
- && left.owners.every((owner, index) => owner.record.token === right.owners[index].record.token
- && sameFingerprint(owner.fingerprint, right.owners[index].fingerprint));
+ return left.record.token === right.record.token
+ && sameFileIdentity(left.lockIdentity, right.lockIdentity)
+ && sameFingerprint(left.fingerprint, right.fingerprint);
}
-/**
- * Reclaim requires every owner record to be past the stale window and to name a dead PID.
- * Any live, fresh or unaccounted entry keeps the lock: age alone never evicts a live holder,
- * and an entry this code cannot attribute might belong to an owner it cannot judge.
- */
function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
const observed = readShimRestoreLockSnapshot(path);
if (!observed) return false;
- const now = Date.now();
- for (const owner of observed.owners) {
- const createdAt = Math.max(owner.record.createdAt, owner.fingerprint.mtimeMs);
- if (now - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
- if (isProcessAlive(owner.record.pid)) return false;
- }
+ const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
+ if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
+ if (isProcessAlive(observed.record.pid)) return false;
const current = readShimRestoreLockSnapshot(path);
if (!current || !sameShimRestoreLock(observed, current)) return false;
beforeDelete?.();
try {
- // The token is part of each owner filename. Even if the lock directory is
- // replaced after the comparison, these unlinks cannot target a successor's
- // differently named owner record. A failure part-way leaves a smaller set
- // of the same dead owners, which the next attempt reclaims.
- for (const owner of observed.owners) unlinkSync(owner.ownerPath);
+ // The token is part of the owner filename. Even if the lock directory is
+ // replaced after the comparison, this unlink cannot target a successor's
+ // differently named owner record.
+ unlinkSync(observed.ownerPath);
rmdirSync(path);
return true;
} catch {
@@ -169,7 +132,7 @@ function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreL
released = true;
try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
try {
- const current = soleShimRestoreLockOwner(path);
+ const current = readShimRestoreLockSnapshot(path);
if (identity && current && current.record.token === record.token
&& sameFileIdentity(identity, current.fingerprint.identity)) {
unlinkSync(ownerPath);
@@ -182,7 +145,7 @@ function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreL
if (fd !== null) {
try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
try {
- const current = soleShimRestoreLockOwner(path);
+ const current = readShimRestoreLockSnapshot(path);
if (identity && current && current.record.token === record.token
&& sameFileIdentity(identity, current.fingerprint.identity)) {
unlinkSync(ownerPath);
diff --git a/src/config/provider-validation.ts b/src/config/provider-validation.ts
index 1ba0b7decbc..e06104c0709 100644
--- a/src/config/provider-validation.ts
+++ b/src/config/provider-validation.ts
@@ -13,7 +13,6 @@ import {
isWirePinnedModel,
MODEL_ADAPTER_OVERRIDE_ALLOWED,
REASONING_SUMMARY_DELIVERY_VALUES,
- THINKING_DISPLAY_VALUES,
UPSTREAM_HTTP_VERSION_VALUES,
type OcxProviderConfig,
type ModelCapabilities,
@@ -30,7 +29,6 @@ const SENSITIVE_PROVIDER_HEADERS = new Set([
"x-amz-security-token",
]);
const REASONING_SUMMARY_DELIVERY_SET = new Set(REASONING_SUMMARY_DELIVERY_VALUES);
-const THINKING_DISPLAY_SET = new Set(THINKING_DISPLAY_VALUES);
const DISPLAY_NAME_CONTROL_CHARS = /[\u0000-\u001f\u007f-\u009f\u2028\u2029]/;
const MAX_MODEL_DISPLAY_NAME_LENGTH = 128;
@@ -387,33 +385,6 @@ export function reasoningSummaryDeliveryRecordConfigError(
return null;
}
-/** Validate `thinkingDisplay`: one closed vocabulary shared by config and management writes. */
-export function thinkingDisplayConfigError(value: unknown, field = "thinkingDisplay"): string | null {
- if (value === undefined) return null;
- if (typeof value !== "string" || !THINKING_DISPLAY_SET.has(value)) {
- return `${field} must be one of: ${THINKING_DISPLAY_VALUES.join(", ")}`;
- }
- return null;
-}
-
-/** Validate `modelThinkingDisplay`: a plain map of model id to the same closed vocabulary. */
-export function modelThinkingDisplayConfigError(value: unknown, field = "modelThinkingDisplay"): string | null {
- if (value === undefined) return null;
- if (!value || typeof value !== "object" || Array.isArray(value)) return `${field} must be a plain object`;
- const prototype = Object.getPrototypeOf(value);
- if (prototype !== Object.prototype && prototype !== null) return `${field} must be a plain object with own properties`;
- for (const [key, entry] of Object.entries(value)) {
- const safeKey = JSON.stringify(redactSecretString(key));
- if (!key.trim()) return `${field} keys must be nonblank model ids`;
- if (["__proto__", "prototype", "constructor"].includes(key)) {
- return `${field} keys must not be reserved object keys`;
- }
- const entryError = thinkingDisplayConfigError(entry, `${field}.${safeKey}`);
- if (entryError) return entryError;
- }
- return null;
-}
-
/** Validate a provider's per-model wire override map against runtime routing rules. */
export function modelAdapterRecordConfigError(
value: unknown,
diff --git a/src/config/schema/config-schema.ts b/src/config/schema/config-schema.ts
index 3838c5a949e..b57c3efa537 100644
--- a/src/config/schema/config-schema.ts
+++ b/src/config/schema/config-schema.ts
@@ -60,8 +60,6 @@ import {
providerHeadersConfigError,
providerForwardClientHeadersConfigError,
reasoningSummaryDeliveryRecordConfigError,
- thinkingDisplayConfigError,
- modelThinkingDisplayConfigError,
credentialPinnedProviderConfigError,
} from "../provider-validation";
import {
@@ -769,26 +767,6 @@ export const configSchema = z.object({
message: reasoningSummaryDeliveryError,
});
}
- const thinkingDisplayError = thinkingDisplayConfigError(
- (provider as { thinkingDisplay?: unknown }).thinkingDisplay,
- );
- if (thinkingDisplayError) {
- ctx.addIssue({
- code: "custom",
- path: ["providers", redactSecretString(name), "thinkingDisplay"],
- message: thinkingDisplayError,
- });
- }
- const modelThinkingDisplayError = modelThinkingDisplayConfigError(
- (provider as { modelThinkingDisplay?: unknown }).modelThinkingDisplay,
- );
- if (modelThinkingDisplayError) {
- ctx.addIssue({
- code: "custom",
- path: ["providers", redactSecretString(name), "modelThinkingDisplay"],
- message: modelThinkingDisplayError,
- });
- }
const minMaxOutputError = positiveIntegerConfigError(
(provider as { minMaxOutputTokens?: unknown }).minMaxOutputTokens,
"minMaxOutputTokens",
diff --git a/src/config/schema/leaf-validators.ts b/src/config/schema/leaf-validators.ts
index 594f910c11d..5df3213e410 100644
--- a/src/config/schema/leaf-validators.ts
+++ b/src/config/schema/leaf-validators.ts
@@ -1,5 +1,4 @@
import * as z from "zod/v4";
-import { EXPORT_CLIENT_IDS } from "../../clients/client-ids";
import { accountAutoPausePolicySchema, accountPauseDetailsSchema } from "./account-auto-pause";
import { anthropicAccountPoolSchema } from "./anthropic-account-pool";
import { join } from "node:path";
@@ -30,7 +29,6 @@ import {
MODEL_ADAPTER_OVERRIDE_ALLOWED,
pinnedWireAdapter,
PROVIDER_WEB_SEARCH_BRIDGE_BACKENDS,
- THINKING_DISPLAY_VALUES,
UPSTREAM_HTTP_VERSION_VALUES,
type OcxProviderConfig,
type OcxConfig,
@@ -362,10 +360,6 @@ export const providerConfigSchema = z.object({
inferContextWindow: z.boolean().optional(),
modelSupportsServiceTier: z.record(z.string().min(1), z.boolean()).optional(),
modelSuppressSyntheticMax: z.record(z.string().min(1), z.boolean()).optional(),
- // Validated against a closed vocabulary here as well as at the write boundaries, so a
- // hand-edited typo cannot load silently and leave the operator with the built-in display (#8363).
- thinkingDisplay: z.enum(THINKING_DISPLAY_VALUES).optional(),
- modelThinkingDisplay: z.record(z.string().min(1), z.enum(THINKING_DISPLAY_VALUES)).optional(),
promoteAdditionalTools: z.boolean().optional(),
preserveResponsesReasoningContent: z.boolean().optional(),
preserveResponsesInputItemIds: z.boolean().optional(),
@@ -963,7 +957,7 @@ export const remoteGuiConfigSchema = z.object({
allowInsecureHttp: z.boolean().optional(),
}).strict();
-const connectedClientIdSchema = z.enum(["codex", "claude", "claude-desktop", "grok", ...EXPORT_CLIENT_IDS]);
+const connectedClientIdSchema = z.enum(["codex", "claude"]);
const clientTimestampSchema = z.string().datetime({ offset: true });
const clientTransportSchema = z.enum(["hub", "link"]);
const linkTransportSchema = z.object({
@@ -986,7 +980,7 @@ export const clientConnectionSchema = z.object({
managementTransport: z.enum(["direct", "relay"]),
transport: clientTransportSchema.optional(),
link: linkTransportSchema.optional(),
- selectedClients: z.array(connectedClientIdSchema).superRefine((clients, ctx) => {
+ selectedClients: z.array(connectedClientIdSchema).min(1).max(2).superRefine((clients, ctx) => {
if (new Set(clients).size !== clients.length) {
ctx.addIssue({ code: "custom", message: "must contain unique client ids" });
}
diff --git a/src/integrations/owned-refresh.ts b/src/integrations/owned-refresh.ts
index 8885f64c595..7f9e848e9a7 100644
--- a/src/integrations/owned-refresh.ts
+++ b/src/integrations/owned-refresh.ts
@@ -44,14 +44,8 @@ export interface OwnedIntegrationRefreshOutcome {
}
/**
- * Returns `null` when the client has never been connected by OpenCodex, or
- * when it was connected and has since been uninstalled. Callers use that
- * distinction to report "left alone" rather than "skipped".
- *
- * An uninstalled client keeps its ownership record: an implicit refresh must
- * not edit ownership, and a reinstall resumes refreshing. Reporting it as a
- * failure instead turned every model toggle into a "pi is not installed"
- * warning for a client the user no longer has.
+ * Returns `null` when the client has never been connected by OpenCodex.
+ * Callers use that distinction to report "left alone" rather than "skipped".
*/
export async function refreshOwnedIntegration(
input: OwnedIntegrationRefreshInput,
@@ -78,7 +72,6 @@ export async function refreshOwnedIntegration(
input.io?.now ?? Date.now,
() => refreshIntegrationCoordinated(bound, options),
);
- if (!result.ok && result.reason === "not_installed") return null;
return result.ok
? {
client: input.clientId,
diff --git a/src/integrations/route-intent-catalog.ts b/src/integrations/route-intent-catalog.ts
deleted file mode 100644
index 161f1c5df09..00000000000
--- a/src/integrations/route-intent-catalog.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-/** Passive routing facts only. No catalog discovery, persistence, targets, or credentials. */
-import { comboPublicModelId, isValidComboId } from "../combos/identifiers";
-import type { OcxConfig } from "../types";
-
-export const MAX_ROUTE_CATALOG_BYTES = 1024 * 1024;
-export interface RouteCatalogFacts { version: 1; combos: Array<{ publicId: string; comboId: string }> }
-export function parseRouteCatalogFacts(raw: unknown): RouteCatalogFacts | null {
- if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join(",") !== "combos,version"
- || !("version" in raw) || raw.version !== 1 || !("combos" in raw) || !Array.isArray(raw.combos) || raw.combos.length > 4096) return null;
- const seen = new Set();
- const combos: RouteCatalogFacts["combos"] = [];
- for (const row of raw.combos) {
- if (!row || typeof row !== "object" || Array.isArray(row) || Object.keys(row).sort().join(",") !== "comboId,publicId"
- || typeof row.comboId !== "string" || !isValidComboId(row.comboId) || typeof row.publicId !== "string"
- || !row.publicId || row.publicId.length > 512 || /[\x00-\x1f\x7f]/.test(row.publicId) || seen.has(row.publicId)) return null;
- seen.add(row.publicId); combos.push({ publicId: row.publicId, comboId: row.comboId });
- }
- return { version: 1, combos };
-}
-export function projectRouteCatalogFacts(config: OcxConfig): RouteCatalogFacts {
- const facts = parseRouteCatalogFacts({ version: 1, combos: Object.entries(config.combos ?? {}).map(([comboId, combo]) => ({
- publicId: comboPublicModelId(comboId, combo), comboId,
- })) });
- if (!facts || Buffer.byteLength(JSON.stringify(facts)) > MAX_ROUTE_CATALOG_BYTES) throw new Error("Route catalog facts are invalid or too large");
- return facts;
-}
diff --git a/src/integrations/route-intent-connected.ts b/src/integrations/route-intent-connected.ts
deleted file mode 100644
index e2669673eac..00000000000
--- a/src/integrations/route-intent-connected.ts
+++ /dev/null
@@ -1,167 +0,0 @@
-/** Read-only Home fetches, followed by local materialization under the caller's sync fence. */
-import type { ExportModel } from "../clients/config-export";
-import { loadConfig } from "../config";
-import { withClientLifecycleSync } from "../client/lifecycle-lock";
-import { nativeOpenAiSlugs } from "../codex/catalog/metadata";
-import { COMBO_NAMESPACE } from "../combos/identifiers";
-import type { LocalRouteIntentOptions } from "../client/route-intent-apply";
-import { clearHomeRouteIntentRecord, fetchHomeRouteIntent, homeRouteIntentReceipt, linkBoundFetch, readHomeRouteIntentRecord,
- writeHomeRouteIntentRecord, type LinkHomeConnection, type LinkHomeDeps } from "../client/link-home";
-import { readBoundedResponseBytes } from "../lib/bounded-body";
-import { ROUTE_CLIENT_IDS, type HomeRouteIntentDocument } from "../link/route-intent";
-import type { OcxClientConnectionConfig } from "../types";
-import type { RouteIntentReceipt } from "./route-intent-contract";
-import { effectiveRouteIntent, readLocalRouteIntentOff, readRouteIntentReceipt, writeRouteIntentReceipt } from "./route-intent-state";
-import { resolveInitialRouteIntent, initialRouteOutcomeMetadata } from "./route-intent-initial";
-import { LINK_ROUTE_CATALOG_PATH } from "../link/management-routes";
-import { MAX_ROUTE_CATALOG_BYTES, parseRouteCatalogFacts } from "./route-intent-catalog";
-
-export interface PreparedConnectedRouteIntent { document: HomeRouteIntentDocument; fetchedAt: string; stale: boolean; localOnly?: true }
-
-/** Provisioning preview; connect records its enabled-only choices after enrollment. */
-export function initialConnectRouteIntent(
- prepared: PreparedConnectedRouteIntent | undefined, selected: OcxClientConnectionConfig["selectedClients"], explicit = false,
-): PreparedConnectedRouteIntent | undefined {
- if (!prepared || !explicit) return prepared;
- const document = structuredClone(prepared.document);
- for (const client of ROUTE_CLIENT_IDS) {
- const enabled = selected.includes(client);
- if (client === "codex" || client === "claude" || enabled) {
- document.clients[client] = { ...document.clients[client], enabled };
- document.sources[client] = "device";
- }
- }
- return { ...prepared, document };
-}
-
-export async function prepareConnectedRouteIntent(
- connection: LinkHomeConnection, token: string, deps?: LinkHomeDeps, beforeWrite?: () => void,
-): Promise {
- if (connection.transport !== "link" || !connection.link) return undefined;
- const fetched = await fetchHomeRouteIntent(connection, token, deps);
- if (fetched.kind === "fresh") return { document: resolveInitialRouteIntent(connection.link.linkId, fetched.document, beforeWrite), fetchedAt: new Date().toISOString(), stale: false };
- const previous = fetched.kind === "failed" ? readHomeRouteIntentRecord(connection.link.linkId) : null;
- return previous ? { document: resolveInitialRouteIntent(connection.link.linkId, previous.document, beforeWrite), fetchedAt: previous.fetchedAt, stale: true } : undefined;
-}
-
-export function connectedRouteSelectedClients(prepared: PreparedConnectedRouteIntent | undefined, fallback: OcxClientConnectionConfig["selectedClients"]) {
- const off = new Set(readLocalRouteIntentOff());
- return prepared ? effectiveRouteIntent(prepared.document).selectedClients : fallback.filter(id => !off.has(id));
-}
-
-/** Read the exact /v1/models rows this Child relays, preserving ids byte-for-byte. */
-export async function loadConnectedRouteModels(connection: LinkHomeConnection, token: string, deps: LinkHomeDeps = {}): Promise {
- if (!connection.link) throw new Error("A Link connection is required for the local served roster");
- const fetchImpl = deps.fetchImpl ?? linkBoundFetch(connection, token);
- const response = await fetchImpl(`http://127.0.0.1:${connection.link.tunnelPort}/v1/models`, {
- method: "GET", headers: { authorization: `Bearer ${token}` }, redirect: "manual", signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000),
- });
- if (!response.ok) { await response.body?.cancel(); throw new Error("The Child served model roster is unavailable"); }
- const body = await readBoundedResponseBytes(response, { maxBytes: 8 * 1024 * 1024, signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000) });
- if (body.oversized) throw new Error("The Child served model roster is too large");
- const raw: unknown = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body.bytes));
- if (!raw || typeof raw !== "object" || !("data" in raw) || !Array.isArray(raw.data)) throw new Error("Invalid Child served model roster");
- const seen = new Set();
- const nativeIds = new Set(nativeOpenAiSlugs());
- const comboIdentity = new Map();
- if (raw.data.some(row => row && typeof row === "object" && row.is_combo === true)) {
- // is_combo describes the inbound adapter in owned_by. This separate passive projection
- // contains only public selectors and canonical combo keys, never model discovery.
- const response = await fetchImpl(`http://127.0.0.1:${connection.link.tunnelPort}${LINK_ROUTE_CATALOG_PATH}`, {
- method: "GET", headers: { authorization: `Bearer ${token}` }, redirect: "manual", signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000),
- });
- if (!response.ok) { await response.body?.cancel(); throw new Error("Combo route identity is unavailable"); }
- const body = await readBoundedResponseBytes(response, { maxBytes: MAX_ROUTE_CATALOG_BYTES, signal: AbortSignal.timeout(deps.timeoutMs ?? 10_000) });
- if (body.oversized) throw new Error("Combo metadata is too large");
- const facts = parseRouteCatalogFacts(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(body.bytes)));
- if (!facts) throw new Error("Invalid combo route metadata");
- for (const row of facts.combos) comboIdentity.set(row.publicId, row.comboId);
- }
- return raw.data.map((row: unknown): ExportModel => {
- if (!row || typeof row !== "object" || !("id" in row) || typeof row.id !== "string" || !row.id || seen.has(row.id)) {
- throw new Error("Invalid or duplicate Child served model identifier");
- }
- seen.add(row.id);
- const owner = "owned_by" in row ? row.owned_by : undefined;
- if ("is_combo" in row && row.is_combo === true) {
- const id = comboIdentity.get(row.id);
- if (!id) throw new Error("The served combo has no verified route identity");
- return { namespaced: row.id, provider: COMBO_NAMESPACE, id };
- }
- const canonical = "native_id" in row ? row.native_id : row.id;
- if (typeof canonical !== "string" || !canonical || canonical !== canonical.trim() || canonical.length > 4096 || /[\x00-\x1f\x7f]/.test(canonical)) {
- throw new Error("Invalid canonical served model identity");
- }
- const slash = canonical.indexOf("/");
- const native = slash < 0 && owner !== COMBO_NAMESPACE && (nativeIds.has(canonical) || owner === "openai");
- if (slash < 0 && !native) throw new Error("The served alias has no verified canonical route identity");
- if (!native && (slash === 0 || slash === canonical.length - 1)) throw new Error("Invalid canonical routed model identity");
- return { namespaced: row.id, provider: native ? "openai" : canonical.slice(0, slash),
- id: native ? canonical : canonical.slice(slash + 1), ...(native ? { native: true } : {}) };
- });
-}
-
-export async function applyConnectedRouteIntent(
- connection: OcxClientConnectionConfig, token: string, prepared: PreparedConnectedRouteIntent | undefined,
- options: LocalRouteIntentOptions, deps: LinkHomeDeps = {},
-): Promise {
- if (!connection.link) return undefined;
- const localOff = readLocalRouteIntentOff(options.statePath);
- if (!prepared && localOff.length > 0) {
- const previous = readRouteIntentReceipt(options.statePath);
- const document: HomeRouteIntentDocument = previous?.document ?? { version: 1, home: { name: "Home", version: "unavailable" },
- clients: Object.fromEntries(ROUTE_CLIENT_IDS.map(id => [id, { enabled: connection.selectedClients.includes(id) }])) as HomeRouteIntentDocument["clients"],
- sources: Object.fromEntries(ROUTE_CLIENT_IDS.map(id => [id, "builtin"])) as HomeRouteIntentDocument["sources"] };
- prepared = { document, stale: true, fetchedAt: new Date().toISOString(), localOnly: true };
- options = { ...options, onlyClients: localOff };
- }
- if (!prepared) return undefined;
- const { materializeLocalRouteIntent } = await import("../client/route-intent-apply");
- const intent = prepared;
- const linkId = connection.link.linkId;
- const guard = options.beforeWrite ?? (() => {});
- const previousModels = readRouteIntentReceipt(options.statePath)?.models ?? [];
- const local = loadConfig();
- const config = { ...local, hostname: "127.0.0.1", unauthenticatedLoopbackListener: undefined };
- const previousHomeRecord = readHomeRouteIntentRecord(linkId);
- const { createIntegrationStateStore } = await import("./store");
- const { captureFileIntentOriginals } = await import("../client/route-intent-file-originals");
- const store = options.store ?? createIntegrationStateStore();
- withClientLifecycleSync(() => {
- guard();
- // Write-ahead, before any client changes: which file integrations were standalone.
- captureFileIntentOriginals(linkId, readRouteIntentReceipt(options.statePath)?.linkId === undefined, store.readRecordsStrict());
- // A failed materialization must not leave the separately reportable Home proof current.
- clearHomeRouteIntentRecord();
- }, options.lifecycleLockDeps);
- const persist = (receipt: RouteIntentReceipt) => {
- receipt.outcomes = receipt.outcomes.map(initialRouteOutcomeMetadata(linkId, intent.document));
- withClientLifecycleSync(() => {
- guard();
- writeRouteIntentReceipt(receipt, options.statePath);
- if (intent.localOnly) { clearHomeRouteIntentRecord(); return; }
- const record = homeRouteIntentReceipt(linkId, intent.document, intent.fetchedAt, receipt.appliedAt, receipt.outcomes, previousHomeRecord);
- try { writeHomeRouteIntentRecord(record); }
- catch (error) { clearHomeRouteIntentRecord(); throw error; }
- }, options.lifecycleLockDeps);
- };
- // Disable and opt-out need no catalog. Their proven outcomes survive an offline Home;
- // every enabled client stays stale until its local served model roster is available.
- const effective = effectiveRouteIntent(intent.document, options.statePath);
- const off = ROUTE_CLIENT_IDS.filter(id => !effective.clients[id].enabled && (!options.onlyClients || options.onlyClients.includes(id)));
- const receipt = await materializeLocalRouteIntent(config, intent.document, previousModels, { ...options, onlyClients: off });
- persist(receipt);
- if (intent.localOnly) { receipt.catalogPending = true; persist(receipt); return receipt; }
- if (off.length === ROUTE_CLIENT_IDS.length) return receipt;
- let models: ExportModel[];
- try { models = await loadConnectedRouteModels(connection, token, deps); }
- catch {
- guard();
- receipt.catalogPending = true;
- persist(receipt);
- return receipt;
- }
- const applied = await materializeLocalRouteIntent(config, intent.document, models, options);
- persist(applied);
- return applied;
-}
diff --git a/src/integrations/route-intent-contract.ts b/src/integrations/route-intent-contract.ts
deleted file mode 100644
index 1c56a98bef2..00000000000
--- a/src/integrations/route-intent-contract.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { ExportModel } from "../clients/config-export";
-import type { HomeRouteIntentDocument, RouteClientId, RouteClientIntent, RouteClientOutcome } from "../link/route-intent";
-import type { OcxConfig } from "../types";
-import type { IntegrationStateStore } from "./store";
-
-export type { RouteClientOutcome } from "../link/route-intent";
-
-export interface RouteIntentReceipt {
- version: 1;
- linkId?: string;
- /** Last local served roster, retained for offline owned-conflict recovery. No credentials. */
- models?: readonly ExportModel[];
- document: HomeRouteIntentDocument;
- clients: Record;
- sources: Record;
- selectedClients: RouteClientId[];
- outcomes: RouteClientOutcome[];
- /** Null if any client failed, conflicts, or is not installed. Never a partial success. */
- appliedRouteIntentDigest: string | null;
- appliedAt: string;
- /** Off intents are proved, while enabled clients await the local served model roster. */
- catalogPending?: true;
-}
-
-export interface RouteIntentApplyInput {
- config: OcxConfig;
- linkId?: string;
- document: HomeRouteIntentDocument;
- /** The local served roster, never Home-generated client contributions. */
- models: readonly ExportModel[];
- home?: string;
- env?: NodeJS.ProcessEnv;
- store?: IntegrationStateStore;
- /** Isolates local opt-outs and receipts; defaults to the local config directory. */
- statePath?: string;
- /** Home passes an empty list; undefined reads this Child's persistent local opt-outs. */
- localOff?: readonly RouteClientId[];
- overwriteConflict?: boolean;
- /** When a client toggle grants takeover, other clients retain ordinary conflict checks. */
- overwriteClient?: RouteClientId;
- /** Rechecks connection/runtime ownership immediately before each writer commits. */
- beforeWrite?: () => void;
- /** Reconcile only explicit local opt-outs when no Home document is available. */
- onlyClients?: readonly RouteClientId[];
- /** Clients skipped by onlyClients keep the previous receipt outcome for the same client intent. */
- retainSkippedOutcomes?: boolean;
- /** Explicit disconnect recovery only: snapshot an owned conflicting block, then remove it. */
- recoverConflicts?: boolean;
- /** Lifecycle exclusion around async file writers; native adapters manage their own lock order. */
- withFileWrites?: (work: () => Promise) => Promise;
- withReceiptWrite?: (write: () => void) => void;
- now?: () => Date;
- /** Native adapters retain their existing ownership/journal implementations. */
- applyNative?: (client: "codex" | "claude" | "claude-desktop" | "grok", intent: RouteClientIntent) => Promise;
-}
diff --git a/src/integrations/route-intent-disconnect.ts b/src/integrations/route-intent-disconnect.ts
deleted file mode 100644
index 2ec9d6e288d..00000000000
--- a/src/integrations/route-intent-disconnect.ts
+++ /dev/null
@@ -1,102 +0,0 @@
-/** File clients drain before disconnect removes its connection and service credential. */
-import { loadConfig } from "../config";
-import { lstatSync } from "node:fs";
-import { readClientConnectionState, sameClientConnectionOwner } from "../client/state";
-import { withClientLifecycle, withClientLifecycleSync, type ClientLifecycleLockDeps } from "../client/lifecycle-lock";
-import { readDesktopDisconnectReceipt, type DesktopDisconnectReceipt } from "../claude/desktop-remote-store";
-import { readServiceApiTokenState } from "../lib/service-secrets";
-import { readLocalRouteIntentOff, readRouteIntentReceiptStrict } from "./route-intent-state";
-import { homeRouteIntentPath } from "../client/link-home";
-import type { OcxClientConnectionConfig } from "../types";
-
-/** Droid's empty-roster contribution has no selectors, so its generic classifier cannot prove absence. */
-async function droidRoutingAbsent(port: number): Promise {
- const { INTEGRATION_CLIENTS } = await import("./registry");
- const { defaultIntegrationIO, loadTarget } = await import("./config-io");
- const { createIntegrationStateStore } = await import("./store");
- const target = loadTarget(defaultIntegrationIO(createIntegrationStateStore()), INTEGRATION_CLIENTS.droid.configPath());
- if (!target.ok) return false;
- if (target.before === null) return true;
- try {
- const value: unknown = JSON.parse(target.before);
- if (!value || typeof value !== "object" || Array.isArray(value)) return false;
- const rows = (value as Record).customModels;
- if (rows === undefined) return true;
- if (!Array.isArray(rows)) return false;
- return rows.every(row => {
- if (!row || typeof row !== "object" || Array.isArray(row)) return false;
- if (typeof row.displayName === "string" && row.displayName.startsWith("OpenCodex:")) return false;
- if (typeof row.baseUrl !== "string") return false;
- const url = new URL(row.baseUrl);
- return !(["127.0.0.1", "localhost", "[::1]"].includes(url.hostname) && url.port === String(port));
- });
- } catch { return false; }
-}
-
-export async function cleanupConnectedRouteIntent(
- connection: OcxClientConnectionConfig, expected: DesktopDisconnectReceipt, lifecycleLockDeps?: ClientLifecycleLockDeps, recoverConflicts = false,
-): Promise {
- const receipt = readRouteIntentReceiptStrict();
- if (!receipt) {
- let homeReceiptPresent = false;
- try { lstatSync(homeRouteIntentPath()); homeReceiptPresent = true; }
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
- if (connection.link) {
- const { createIntegrationStateStore } = await import("./store");
- const { isIntegrationClientId } = await import("./registry");
- const { readIntegrationState } = await import("./state");
- const { listAsideProfileStores } = await import("./aside-profile-context");
- const store = createIntegrationStateStore();
- const owned = Object.keys(store.readRecordsStrict()).length > 0
- || listAsideProfileStores(store).some(child => Object.keys(child.store.readRecordsStrict()).length > 0);
- const config = loadConfig();
- // Older Homes and --no-sync may never invoke the controller. Only disk absence,
- // not selectedClients alone, proves those file clients have nothing to drain.
- const unproved = connection.selectedClients.some(id => id !== "codex" && id !== "claude"
- && (!isIntegrationClientId(id) || readIntegrationState({ clientId: id, config, port: config.port, models: [], store }).state !== "absent"));
- const droidUnproved = connection.selectedClients.includes("droid") && !await droidRoutingAbsent(config.port);
- if (homeReceiptPresent || owned || unproved || droidUnproved) {
- throw new Error("Route intent cleanup receipt is missing; preserve the connection and repair its route ownership evidence before disconnect");
- }
- }
- return;
- }
- // A valid unbound receipt is standalone Home state, not evidence of a Child apply.
- if (receipt.linkId === undefined) return;
- if (!connection.link || receipt.linkId !== connection.link.linkId) throw new Error("Route intent cleanup receipt belongs to another context; preserve it before disconnect");
- const guard = () => {
- const pending = readDesktopDisconnectReceipt();
- const current = readClientConnectionState();
- const token = readServiceApiTokenState();
- if (pending.kind !== "valid" || JSON.stringify(pending.value) !== JSON.stringify(expected)
- || current.kind !== "connected" || !sameClientConnectionOwner(current.value, connection)
- || current.value.pendingOperation || token.kind !== "present" || token.fingerprint !== connection.tokenFingerprint) {
- throw new Error("client_disconnect_owner_changed");
- }
- };
- const local = loadConfig();
- const { cleanupRouteIntent } = await import("./route-intent");
- const { stripGrokConfig } = await import("../grok/inject");
- const { injectClaudeAgentDefs } = await import("../claude/agents-inject");
- const config = { ...local, hostname: "127.0.0.1", unauthenticatedLoopbackListener: undefined };
- // Integrations the Child owned before this link come back instead of being removed; a local
- // opt-out is the person's own later choice and wins.
- const { readFileIntentOriginals } = await import("../client/route-intent-file-originals");
- const originals = readFileIntentOriginals();
- const optedOut = new Set(readLocalRouteIntentOff());
- const standalone = originals?.linkId === connection.link.linkId
- ? Object.fromEntries(Object.entries(originals.clients).filter(([client]) => !optedOut.has(client))) : {};
- const outcomes = await cleanupRouteIntent({ config, linkId: connection.link.linkId, document: receipt.document, models: receipt.models ?? [], beforeWrite: guard, recoverConflicts,
- withReceiptWrite: write => withClientLifecycleSync(() => { guard(); write(); }, lifecycleLockDeps),
- withFileWrites: work => withClientLifecycle(work, lifecycleLockDeps) }, standalone);
- if (outcomes.some(row => row.outcome !== "current")) throw new Error("Route intent cleanup was partial; ownership and disconnect receipts retained. Repair the conflicting client and rerun ocx disconnect.");
- withClientLifecycleSync(() => {
- guard();
- const grok = stripGrokConfig();
- if (!grok.ok) throw new Error("Grok route cleanup was partial; rerun ocx disconnect after repair");
- const config = loadConfig();
- const agents = injectClaudeAgentDefs({ ...config, claudeCode: { ...config.claudeCode, enabled: false } }, {});
- if (agents === null) throw new Error("Claude route cleanup was partial; rerun ocx disconnect after repair");
- guard();
- }, lifecycleLockDeps);
-}
diff --git a/src/integrations/route-intent-initial.ts b/src/integrations/route-intent-initial.ts
deleted file mode 100644
index 73cd8030a07..00000000000
--- a/src/integrations/route-intent-initial.ts
+++ /dev/null
@@ -1,64 +0,0 @@
-/** Provisioning choices are local enabled-only overrides, never writes to Home policy. */
-import { mkdirSync } from "node:fs";
-import { join } from "node:path";
-import { atomicWriteFileNoFollow } from "../config/atomic-write";
-import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
-import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
-import { isRouteClientId, ROUTE_CLIENT_IDS, type HomeRouteIntentDocument, type RouteClientId, type RouteClientOutcome } from "../link/route-intent";
-import { routeIntentStatePath } from "./route-intent-state";
-
-type InitialSelection = Partial>;
-function recordPath(linkId: string, root: string): string {
- if (!/^lnk_[0-9a-f]{16}$/.test(linkId)) throw new Error("Invalid initial route intent link identity");
- return join(root, `initial-${linkId}.json`);
-}
-export function readInitialRouteIntent(linkId: string, root = routeIntentStatePath()): InitialSelection {
- let raw: unknown;
- try { raw = JSON.parse(readBoundedRegularFileSync(recordPath(linkId, root), 64 * 1024).toString("utf8")); }
- catch (error) { if ((error as NodeJS.ErrnoException).code === "ENOENT") return {}; throw new Error("Initial route selection is unreadable"); }
- if (!raw || typeof raw !== "object" || Array.isArray(raw) || !("version" in raw) || raw.version !== 1
- || !("linkId" in raw) || raw.linkId !== linkId || !("clients" in raw) || !raw.clients || typeof raw.clients !== "object" || Array.isArray(raw.clients)) {
- throw new Error("Invalid initial route selection");
- }
- const clients: InitialSelection = {};
- for (const [id, entry] of Object.entries(raw.clients)) {
- if (!isRouteClientId(id) || !entry || typeof entry !== "object" || Array.isArray(entry)
- || Object.keys(entry).sort().join(",") !== "enabled,revision" || !("enabled" in entry) || typeof entry.enabled !== "boolean"
- || !("revision" in entry) || !Number.isSafeInteger(entry.revision) || Number(entry.revision) < 0) throw new Error("Invalid initial route selection client");
- clients[id] = { enabled: entry.enabled, revision: Number(entry.revision) };
- }
- return clients;
-}
-function writeInitial(linkId: string, clients: InitialSelection, root: string): void {
- assertNotRealHomeUnderTest(root); mkdirSync(root, { recursive: true, mode: 0o700 });
- atomicWriteFileNoFollow(recordPath(linkId, root), JSON.stringify({ version: 1, linkId, clients }));
-}
-export function recordInitialRouteIntent(linkId: string, document: HomeRouteIntentDocument | undefined, selected: readonly RouteClientId[], root = routeIntentStatePath()): void {
- const clients: InitialSelection = {};
- for (const id of ROUTE_CLIENT_IDS) if (id === "codex" || id === "claude" || selected.includes(id)) {
- clients[id] = { enabled: selected.includes(id), revision: document?.clientRevisions?.[id] ?? 0 };
- }
- writeInitial(linkId, clients, root);
-}
-export function resolveInitialRouteIntent(linkId: string, document: HomeRouteIntentDocument, beforeWrite: () => void = () => {}, root = routeIntentStatePath()): HomeRouteIntentDocument {
- const clients = readInitialRouteIntent(linkId, root);
- const next = structuredClone(document);
- let removed = false;
- for (const id of ROUTE_CLIENT_IDS) {
- const initial = clients[id]; if (!initial) continue;
- if ((document.clientRevisions?.[id] ?? 0) !== initial.revision) { delete clients[id]; removed = true; continue; }
- next.clients[id] = { ...next.clients[id], enabled: initial.enabled };
- next.sources[id] = "device";
- }
- if (removed) { beforeWrite(); writeInitial(linkId, clients, root); }
- return next;
-}
-export function initialRouteOutcomeMetadata(linkId: string, document: HomeRouteIntentDocument, root = routeIntentStatePath()) {
- const initial = readInitialRouteIntent(linkId, root);
- return (row: RouteClientOutcome): RouteClientOutcome => {
- const entry = initial[row.client];
- return entry && entry.revision === (document.clientRevisions?.[row.client] ?? 0)
- ? { ...row, initialIntent: { enabled: entry.enabled }, initialRevision: entry.revision } : row;
- };
-}
-export function clearInitialRouteIntent(linkId: string, root = routeIntentStatePath()): void { writeInitial(linkId, {}, root); }
diff --git a/src/integrations/route-intent-state.ts b/src/integrations/route-intent-state.ts
deleted file mode 100644
index 83cae967ba6..00000000000
--- a/src/integrations/route-intent-state.ts
+++ /dev/null
@@ -1,86 +0,0 @@
-import { mkdirSync, unlinkSync } from "node:fs";
-import { join } from "node:path";
-import { atomicWriteFileNoFollow } from "../config/atomic-write";
-import { getConfigDir } from "../config/paths";
-import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
-import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
-import { ROUTE_CLIENT_IDS, isRouteClientId, parseHomeRouteIntentDocument, parseRouteClientIntent, type HomeRouteIntentDocument, type RouteClientId } from "../link/route-intent";
-import type { RouteIntentReceipt } from "./route-intent-contract";
-
-export function routeIntentStatePath(): string { return join(getConfigDir(), "route-intent"); }
-
-function readJson(path: string, maxBytes = 16 * 1024 * 1024): unknown {
- try { return JSON.parse(readBoundedRegularFileSync(path, maxBytes).toString("utf8")); }
- catch (error) {
- if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
- throw new Error("Local route intent state is unreadable; preserve it for recovery");
- }
-}
-
-export function readLocalRouteIntentOff(statePath = routeIntentStatePath()): RouteClientId[] {
- const raw = readJson(join(statePath, "local-off.json"), 64 * 1024);
- if (raw === undefined) return [];
- if (!raw || typeof raw !== "object" || !("version" in raw) || raw.version !== 1 || !("clients" in raw)
- || !Array.isArray(raw.clients) || raw.clients.some(id => !ROUTE_CLIENT_IDS.includes(id))
- || new Set(raw.clients).size !== raw.clients.length) throw new Error("Invalid local route intent opt-outs");
- return raw.clients;
-}
-
-export function effectiveRouteIntent(document: HomeRouteIntentDocument, statePath = routeIntentStatePath(), localOff?: readonly RouteClientId[]) {
- const clients = structuredClone(document.clients);
- const sources: RouteIntentReceipt["sources"] = { ...document.sources };
- for (const id of localOff ?? readLocalRouteIntentOff(statePath)) {
- clients[id] = { ...clients[id], enabled: false };
- sources[id] = "local";
- }
- return { clients, sources, selectedClients: ROUTE_CLIENT_IDS.filter(id => clients[id].enabled) };
-}
-
-export function writeRouteIntentReceipt(receipt: RouteIntentReceipt, statePath = routeIntentStatePath()): void {
- assertNotRealHomeUnderTest(statePath);
- mkdirSync(statePath, { recursive: true, mode: 0o700 });
- const body = JSON.stringify(receipt);
- if (Buffer.byteLength(body) > 16 * 1024 * 1024) throw new Error("Local route intent receipt is too large");
- atomicWriteFileNoFollow(join(statePath, "receipt.json"), body);
-}
-
-export function readRouteIntentReceipt(statePath = routeIntentStatePath()): RouteIntentReceipt | null {
- try { return readRouteIntentReceiptStrict(statePath); } catch { return null; }
-}
-
-/** Cleanup must distinguish absence from corrupt, unsafe, or incomplete ownership evidence. */
-export function readRouteIntentReceiptStrict(statePath = routeIntentStatePath()): RouteIntentReceipt | null {
- const raw = readJson(join(statePath, "receipt.json")) as RouteIntentReceipt | undefined;
- if (raw === undefined) return null;
- if (!raw || raw.version !== 1 || !parseHomeRouteIntentDocument(raw.document, { allowNative: true }) || !Array.isArray(raw.outcomes)
- || raw.outcomes.length !== ROUTE_CLIENT_IDS.length || new Set(raw.outcomes.map(row => row?.client)).size !== ROUTE_CLIENT_IDS.length
- || !raw.clients || !raw.sources || !Array.isArray(raw.selectedClients) || new Set(raw.selectedClients).size !== raw.selectedClients.length
- || raw.selectedClients.some(id => !isRouteClientId(id)) || typeof raw.appliedAt !== "string" || !Number.isFinite(Date.parse(raw.appliedAt))
- || (raw.linkId !== undefined && !/^lnk_[0-9a-f]{16}$/.test(raw.linkId))
- || (raw.models !== undefined && (!Array.isArray(raw.models) || raw.models.some(row => !row || typeof row.namespaced !== "string" || typeof row.provider !== "string" || typeof row.id !== "string")))
- || !(raw.appliedRouteIntentDigest === null || (typeof raw.appliedRouteIntentDigest === "string" && /^[0-9a-f]{64}$/.test(raw.appliedRouteIntentDigest)))
- || raw.outcomes.some(row => !row || !isRouteClientId(row.client) || !["current", "stale", "not_installed", "conflict", "failed"].includes(row.outcome))
- || ROUTE_CLIENT_IDS.some(id => !parseRouteClientIntent(raw.clients[id], id) || !["builtin", "default", "device", "native", "local"].includes(raw.sources[id])
- || raw.selectedClients.includes(id) !== raw.clients[id].enabled)) throw new Error("Route intent receipt is malformed; preserve it and repair before disconnect");
- return raw;
-}
-
-export function clearRouteIntentReceipt(statePath = routeIntentStatePath()): void {
- assertNotRealHomeUnderTest(statePath);
- try { unlinkSync(join(statePath, "receipt.json")); }
- catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; }
-}
-
-/** true opts out; false drops the override and inherits the Home again. Never enables locally. */
-export function setLocalRouteIntentOff(client: RouteClientId, off: boolean, statePath = routeIntentStatePath()): void {
- if (!ROUTE_CLIENT_IDS.includes(client) || typeof off !== "boolean") throw new Error("Invalid local route intent override");
- const clients = new Set(readLocalRouteIntentOff(statePath));
- if (off) clients.add(client); else clients.delete(client);
- assertNotRealHomeUnderTest(statePath);
- mkdirSync(statePath, { recursive: true, mode: 0o700 });
- // Invalidate before publishing the override: failure cannot leave an old success claim.
- const receipt = readRouteIntentReceiptStrict(statePath);
- if (receipt) writeRouteIntentReceipt({ ...receipt, appliedRouteIntentDigest: null,
- outcomes: receipt.outcomes.map(row => row.client === client ? { ...row, outcome: "stale" } : row) }, statePath);
- atomicWriteFileNoFollow(join(statePath, "local-off.json"), JSON.stringify({ version: 1, clients: [...clients].sort() }));
-}
diff --git a/src/integrations/route-intent-tick.ts b/src/integrations/route-intent-tick.ts
deleted file mode 100644
index 3469351c00f..00000000000
--- a/src/integrations/route-intent-tick.ts
+++ /dev/null
@@ -1,65 +0,0 @@
-import type { OcxClientConnectionConfig } from "../types";
-
-export interface RouteIntentTickDeps {
- sync?: () => Promise;
- onApplied?: () => void | Promise;
- setTimer?: (callback: () => void, ms: number) => unknown;
- clearTimer?: (timer: unknown) => void;
-}
-export interface RouteIntentTick { stop(): void; requestSync(): Promise }
-
-/** Periodic and Home-requested applies share one queue and the existing sync ownership fences. */
-export function startRouteIntentTick(connection: OcxClientConnectionConfig, deps: RouteIntentTickDeps = {}): RouteIntentTick {
- if (connection.transport !== "link" || !connection.link) return { stop() {}, requestSync: async () => { throw new Error("not_a_link_child"); } };
- const linkId = connection.link.linkId;
- const sync = deps.sync ?? (async () => {
- const { syncConnectedClient } = await import("../client/connect");
- return syncConnectedClient({ expect: { kind: "link", linkId } });
- });
- const set = deps.setTimer ?? ((callback, ms) => { const timer = setTimeout(callback, ms); timer.unref(); return timer; });
- const clear = deps.clearTimer ?? (timer => clearTimeout(timer as ReturnType));
- let stopped = false, running = false, queued = false;
- let timer: unknown;
- const waiters: Array<{ resolve: (result: unknown) => void; reject: (error: unknown) => void }> = [];
- const schedule = (ms: number) => {
- if (!stopped) timer = set(() => { timer = undefined; queued = true; void drain(); }, ms);
- };
- const drain = async () => {
- if (stopped || running) return;
- running = true;
- let delay = 60_000;
- try {
- do {
- queued = false;
- const batch = waiters.splice(0);
- try {
- const result = await sync();
- await deps.onApplied?.();
- for (const waiter of batch) waiter.resolve(result);
- delay = 60_000;
- } catch (error) {
- for (const waiter of batch) waiter.reject(error);
- delay = 5_000;
- }
- } while (queued && !stopped);
- } finally { running = false; schedule(delay); }
- };
- schedule(2_000);
- return {
- requestSync() {
- if (stopped) return Promise.reject(new Error("client_runtime_stopping"));
- if (timer !== undefined) clear(timer);
- timer = undefined;
- queued = true;
- const result = new Promise((resolve, reject) => waiters.push({ resolve, reject }));
- void drain();
- return result;
- },
- stop() {
- stopped = true; queued = false;
- if (timer !== undefined) clear(timer);
- timer = undefined;
- for (const waiter of waiters.splice(0)) waiter.reject(new Error("client_runtime_stopping"));
- },
- };
-}
diff --git a/src/integrations/route-intent.ts b/src/integrations/route-intent.ts
deleted file mode 100644
index 5f61eecc898..00000000000
--- a/src/integrations/route-intent.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/** Device-local intent controller. All client mutations go through the existing writers. */
-import { existsSync } from "node:fs";
-import { homedir } from "node:os";
-import { join } from "node:path";
-import { routeClientIntentDigest, routeIntentDigest, ROUTE_CLIENT_IDS, type RouteClientId, type RouteClientIntent } from "../link/route-intent";
-import { isIntegrationClientId, type IntegrationClientId } from "./registry";
-import { readIntegrationState } from "./state";
-import { createIntegrationStateStore } from "./store";
-import { applyIntegrationCoordinated, disableIntegrationCoordinated, overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
- type IntegrationWriteInput, type WriteOutcome } from "./writer";
-import { createAsideProfileContext, asideProfileScope, asideWriteInput, asideRootStore, listAsideProfileStores } from "./aside-profile-context";
-import { asideHomeDir } from "../clients/config-export";
-import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
-import type { RouteClientOutcome, RouteIntentApplyInput, RouteIntentReceipt } from "./route-intent-contract";
-import { effectiveRouteIntent, readRouteIntentReceipt, routeIntentStatePath, writeRouteIntentReceipt } from "./route-intent-state";
-export { clearRouteIntentReceipt, effectiveRouteIntent, readLocalRouteIntentOff, readRouteIntentReceipt, readRouteIntentReceiptStrict, routeIntentStatePath, setLocalRouteIntentOff } from "./route-intent-state";
-export type { RouteClientOutcome, RouteIntentApplyInput, RouteIntentReceipt } from "./route-intent-contract";
-
-const flights = new Set();
-
-function project(client: RouteClientId, result: WriteOutcome, target: string): RouteClientOutcome {
- return { client, target, outcome: result.ok ? "current" : result.reason === "not_installed" ? "not_installed"
- : result.state === "conflict" ? "conflict" : "failed", message: result.message };
-}
-
-async function writeClient(input: RouteIntentApplyInput, bound: IntegrationWriteInput, intent: RouteClientIntent): Promise {
- const state = readIntegrationState(bound);
- // An off intent never adopts an unowned block, including foreign blocks.
- if (!intent.enabled && !bound.store?.readRecords()[bound.clientId]) {
- return { client: bound.clientId, outcome: "current", target: state.configPath };
- }
- if (!intent.enabled && input.recoverConflicts && state.state === "conflict") {
- // A durable record is mandatory above; this cannot adopt an unowned foreign block.
- // Overwrite snapshots the edited preimage through the existing private journal. Unsafe
- // paths, blocked containers and unrepresentable contributions retain their ordinary refusal.
- const recovered = await overwriteIntegrationCoordinated(bound, { guard: () => { input.beforeWrite?.(); return null; } });
- if (!recovered.ok) return project(bound.clientId, recovered, state.configPath);
- }
- const operation = !intent.enabled ? disableIntegrationCoordinated
- : input.overwriteConflict ? overwriteIntegrationCoordinated
- : state.state === "absent" ? applyIntegrationCoordinated : refreshIntegrationCoordinated;
- const result = await operation(bound, { guard: () => { input.beforeWrite?.(); return null; } });
- // A refresh of a deleted owned block returns absent. Intent is still on, so that is stale.
- const outcome = project(bound.clientId, result, state.configPath);
- if (intent.enabled && result.ok && result.state === "absent") outcome.outcome = "stale";
- return outcome;
-}
-
-function manifestText(root: string): string {
- return readBoundedRegularFileSync(join(root, "accounts.json"), 4 * 1024 * 1024).toString("utf8");
-}
-
-async function applyAside(input: RouteIntentApplyInput, intent: RouteClientIntent): Promise {
- const root = asideHomeDir(input.env ?? process.env, input.home ?? homedir());
- const ownershipInput = { ...input, port: input.config.port };
- const rootStore = asideRootStore(ownershipInput);
- const legacy = rootStore.readRecordsStrict().aside;
- const owners = listAsideProfileStores(rootStore).flatMap(child => {
- const records = child.store.readRecordsStrict();
- if (Object.keys(records).some(id => id !== "aside")) throw new Error("Aside profile storage names another client");
- return records.aside ? [{ profileId: child.profileId, record: records.aside }] : [];
- });
- const owned = Boolean(legacy) || owners.length > 0;
- // No owned route means off is already proved, even without a usable account manifest.
- if (!intent.enabled && !owned) return { client: "aside", outcome: "current" };
- if (!existsSync(root)) {
- if (owned) throw new Error("Aside installation is missing with retained route ownership");
- return { client: "aside", outcome: "not_installed" };
- }
- const manifest = manifestText(root);
- const ctx = createAsideProfileContext(ownershipInput);
- // A removed manifest entry is not evidence that its owned file was cleaned up.
- if ((legacy && !ctx.profiles.some(profile => profile.configPath === legacy.configPath))
- || owners.some(owner => !ctx.profiles.some(profile => profile.id === owner.profileId && profile.configPath === owner.record.configPath))) {
- throw new Error("Aside route ownership names a profile outside the account manifest");
- }
- if (manifestText(root) !== manifest) throw new Error("Aside account manifest changed during enumeration");
- const raw: unknown = JSON.parse(manifest);
- // Enumeration intentionally hides userIds. Prove that its entire profile roster was
- // derived from the manifest snapshot whose userIds this operation selects.
- const snapshot = raw as { currentAccountId: number; accounts?: Array<{ id: number; name?: string }> };
- const roster = snapshot.accounts ?? [{ id: snapshot.currentAccountId }];
- if (roster.length !== ctx.profiles.length || ctx.profiles.some((profile, index) => {
- const account = roster[index];
- return !account || account.id !== profile.id || (account.id === snapshot.currentAccountId) !== profile.current
- || (typeof account.name === "string" ? account.name : undefined) !== profile.name;
- })) throw new Error("Aside profile enumeration does not match the account identity snapshot");
- const ids = new Map();
- if (raw && typeof raw === "object" && "accounts" in raw && Array.isArray(raw.accounts)) {
- for (const account of raw.accounts) {
- if (account && typeof account === "object" && Number.isSafeInteger(account.id) && typeof account.userId === "string") {
- ids.set(account.id, account.userId);
- }
- }
- }
- const accounts = intent.accounts ?? "all";
- const selected = ctx.profiles.filter(profile => accounts === "all" || (Array.isArray(accounts) && accounts.includes(ids.get(profile.id) ?? "")));
- const results: RouteClientOutcome[] = [];
- for (const profile of ctx.profiles) {
- const scope = asideProfileScope(ctx, profile);
- const enabled = intent.enabled && selected.includes(profile) && accounts !== "none";
- if (!enabled && !scope.store.readRecords().aside) continue;
- const bound = await asideWriteInput(ctx, scope);
- results.push(await writeClient({ ...input, beforeWrite: () => {
- input.beforeWrite?.(); scope.assertBoundary();
- if (manifestText(root) !== manifest) throw new Error("Aside account identity changed during route apply");
- } }, bound, { ...intent, enabled }));
- }
- const failure = results.find(row => row.outcome === "failed") ?? results.find(row => row.outcome === "conflict")
- ?? results.find(row => row.outcome !== "current");
- if (failure) return { ...failure, client: "aside" };
- if (intent.enabled && accounts !== "none" && selected.length === 0) {
- return { client: "aside", outcome: "not_installed", message: "No local Aside profile matches the requested userIds" };
- }
- return { client: "aside", outcome: "current", target: selected.map(profile => `u${profile.id}`).join(",") };
-}
-
-async function applyClient(input: RouteIntentApplyInput, client: RouteClientId, intent: RouteClientIntent): Promise {
- if (client === "aside") return input.withFileWrites ? input.withFileWrites(() => applyAside(input, intent)) : applyAside(input, intent);
- if (isIntegrationClientId(client)) {
- const write = () => writeClient(input, { clientId: client, config: input.config, port: input.config.port, models: input.models,
- env: input.env, home: input.home, store: input.store, modelNames: intent.modelNames }, intent);
- return input.withFileWrites ? input.withFileWrites(write) : write();
- }
- if (input.applyNative) { input.beforeWrite?.(); return input.applyNative(client, intent); }
- const previous = readRouteIntentReceipt(input.statePath);
- return !intent.enabled && !previous?.clients[client]?.enabled
- ? { client, outcome: "current" }
- : { client, outcome: "failed", message: "This native client needs its existing local ownership adapter" };
-}
-
-/** Home and Child share the same local materializer; absent intent never calls this function. */
-export async function materializeRouteIntent(input: RouteIntentApplyInput): Promise {
- const path = input.statePath ?? routeIntentStatePath();
- if (flights.has(path)) throw new Error("Local route intent apply is already running");
- flights.add(path);
- try {
- input = { ...input, document: structuredClone(input.document), config: structuredClone(input.config), models: structuredClone([...input.models]),
- store: input.store ?? createIntegrationStateStore() };
- const effective = effectiveRouteIntent(input.document, path, input.localOff);
- const previous = input.retainSkippedOutcomes ? readRouteIntentReceipt(path) : null;
- const receipt: RouteIntentReceipt = { version: 1, models: input.models, ...(input.linkId ? { linkId: input.linkId } : {}), document: structuredClone(input.document), ...effective,
- outcomes: ROUTE_CLIENT_IDS.map(client => ({ client, outcome: "stale" })),
- appliedRouteIntentDigest: null, appliedAt: (input.now ?? (() => new Date()))().toISOString() };
- // Withdraw the previous digest before any client can change, including a failed/aborted write.
- const saveReceipt = () => input.withReceiptWrite
- ? input.withReceiptWrite(() => writeRouteIntentReceipt(receipt, path)) : writeRouteIntentReceipt(receipt, path);
- saveReceipt();
- for (const client of ROUTE_CLIENT_IDS) {
- if (input.onlyClients && !input.onlyClients.includes(client)) {
- const kept = previous?.linkId === input.linkId
- ? previous?.outcomes.find(row => row.client === client && row.intentDigest === routeClientIntentDigest(input.document, client)) : undefined;
- if (kept) receipt.outcomes[ROUTE_CLIENT_IDS.indexOf(client)] = kept;
- continue;
- }
- input.beforeWrite?.();
- try {
- receipt.outcomes[ROUTE_CLIENT_IDS.indexOf(client)] = { ...await applyClient({ ...input, overwriteConflict: input.overwriteConflict === true && (input.overwriteClient === undefined || input.overwriteClient === client) }, client, effective.clients[client]),
- intentDigest: routeClientIntentDigest(input.document, client),
- ...(effective.sources[client] === "local" ? { source: "local" as const } : {}) };
- } catch {
- input.beforeWrite?.(); // Ownership loss propagates instead of looking like a disk failure.
- receipt.outcomes[ROUTE_CLIENT_IDS.indexOf(client)] = { client, outcome: "failed", intentDigest: routeClientIntentDigest(input.document, client),
- ...(effective.sources[client] === "local" ? { source: "local" as const } : {}), message: "Local client could not be reconciled safely" };
- }
- }
- input.beforeWrite?.();
- // A local opt-out intentionally differs from the Home document; do not claim its digest.
- const locallyChanged = ROUTE_CLIENT_IDS.some(id => effective.sources[id] === "local" && input.document.clients[id].enabled);
- if (!locallyChanged && receipt.outcomes.every(row => row.outcome === "current")) receipt.appliedRouteIntentDigest = routeIntentDigest(input.document);
- saveReceipt();
- return receipt;
- } finally { flights.delete(path); }
-}
-
-/**
- * Put a standalone integration back: one still owned, or one the person changed (their own block),
- * is kept as is; one the link disabled is applied again. An uninstalled client has nothing to do.
- */
-async function restoreStandaloneClient(input: RouteIntentApplyInput, client: IntegrationClientId, modelNames?: "alias" | "routed"): Promise {
- const bound: IntegrationWriteInput = { clientId: client, config: input.config, port: input.config.port, models: input.models,
- env: input.env, home: input.home, store: input.store, modelNames };
- const state = readIntegrationState(bound);
- if (input.store?.readRecords()[client] || state.state !== "absent") return { client, outcome: "current", target: state.configPath };
- const result = await applyIntegrationCoordinated(bound, { guard: () => { input.beforeWrite?.(); return null; } });
- return result.ok || result.reason === "not_installed" ? { client, outcome: "current", target: state.configPath } : project(client, result, state.configPath);
-}
-
-/**
- * Disconnect calls this while it holds its lifecycle fence, before clearing intent receipts.
- * `standalone` names file clients the Child owned before linking; they are restored, not removed.
- */
-export async function cleanupRouteIntent(input: RouteIntentApplyInput,
- standalone: Partial> = {}): Promise {
- const receipt = readRouteIntentReceipt(input.statePath);
- if (!receipt) return [];
- const document = structuredClone(receipt.document);
- for (const id of ROUTE_CLIENT_IDS) document.clients[id].enabled = false;
- const kept = ROUTE_CLIENT_IDS.filter((id): id is IntegrationClientId => isIntegrationClientId(id) && id !== "aside" && Object.hasOwn(standalone, id));
- const outcomes = (await materializeRouteIntent({ ...input, document, overwriteConflict: false,
- onlyClients: ROUTE_CLIENT_IDS.filter(id => isIntegrationClientId(id) && !(kept as readonly string[]).includes(id)) }))
- .outcomes.filter(row => isIntegrationClientId(row.client) && !(kept as readonly string[]).includes(row.client));
- const store = input.store ?? createIntegrationStateStore();
- for (const client of kept) {
- const restore = () => restoreStandaloneClient({ ...input, store }, client, standalone[client]?.modelNames);
- outcomes.push(input.withFileWrites ? await input.withFileWrites(restore) : await restore());
- }
- return outcomes;
-}
diff --git a/src/lib/bun-runtime-preflight.ts b/src/lib/bun-runtime-preflight.ts
index eb71a7551d4..3f3e7f6a68b 100644
--- a/src/lib/bun-runtime-preflight.ts
+++ b/src/lib/bun-runtime-preflight.ts
@@ -9,7 +9,7 @@ export const RUNTIME_PREFLIGHT_TIMEOUT_MS = 5_000;
export const RUNTIME_PREFLIGHT_GUIDANCE = "The selected Bun runtime could not create and remove a directory in the OpenCodex config directory. "
+ "Windows application policy may deny writes from this executable location. "
+ "Set OPENCODEX_BUN_PATH to a trusted Bun executable allowed by your policy before launching ocx, "
- + "or reinstall opencodex with npm install -g --allow-scripts=bun @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
+ + "or reinstall opencodex with npm install -g @bitkyc08/opencodex, then retry. No alternate runtime was selected.";
export class RuntimePreflightError extends Error {
readonly code = "OCX_RUNTIME_PREFLIGHT_FAILED";
diff --git a/src/link/child-parity.ts b/src/link/child-parity.ts
index bd7cc4d6b53..220ffbc7f43 100644
--- a/src/link/child-parity.ts
+++ b/src/link/child-parity.ts
@@ -14,12 +14,11 @@
*/
import { CODEX_SETTINGS_DIGEST_PATTERN } from "./codex-settings";
import { LINK_MANAGEMENT_PREFIX } from "./management-routes";
-import { parseRouteClientOutcomes, ROUTE_INTENT_DIGEST_PATTERN, routeClientOutcomesForPeer, type RouteClientOutcome } from "./route-intent";
/** The one write a Child may send its Home over the link. */
export const LINK_CHILD_PARITY_PATH = `${LINK_MANAGEMENT_PREFIX}/child-parity`;
/** A report is four short fields; anything larger is refused while it is still being read. */
-export const LINK_CHILD_PARITY_MAX_BYTES = 8192;
+export const LINK_CHILD_PARITY_MAX_BYTES = 1024;
const LINK_ID = /^lnk_[0-9a-f]{16}$/;
/** 2001-09-09 .. 2286-11-20: any epoch-millisecond time a real clock reports. */
@@ -34,8 +33,6 @@ export interface LinkChildParityReport {
runtimeStartedAt: number;
/** `appliedCodexSettingsDigest` of the applied settings; null before any apply. */
appliedCodexSettingsDigest: string | null;
- appliedRouteIntentDigest?: string | null;
- routeClientOutcomes?: RouteClientOutcome[];
/**
* Increases with every report this runtime process sends. With `runtimeStartedAt` it orders
* reports, so a late POST from an older runtime or an earlier send never replaces a newer one.
@@ -48,8 +45,6 @@ export interface LinkChildParity {
lastApplyAt: number | null;
runtimeStartedAt: number | null;
appliedCodexSettingsDigest: string | null;
- appliedRouteIntentDigest?: string | null;
- routeClientOutcomes?: RouteClientOutcome[];
/** Epoch ms the Home received the report (the Home's clock; the other times are the Child's). */
reportedAt: number | null;
}
@@ -70,9 +65,7 @@ function exactKeys(value: unknown, keys: readonly string[]): value is Record = {};
- if (Object.hasOwn(value, "appliedRouteIntentDigest")) {
- const routeDigest = value.appliedRouteIntentDigest;
- if (routeDigest !== null && (typeof routeDigest !== "string" || !ROUTE_INTENT_DIGEST_PATTERN.test(routeDigest))) return null;
- const outcomes = parseRouteClientOutcomes(value.routeClientOutcomes);
- if (!outcomes) return null;
- route = { appliedRouteIntentDigest: routeDigest, routeClientOutcomes: routeClientOutcomesForPeer(outcomes) };
- }
- return { linkId, lastApplyAt, runtimeStartedAt, appliedCodexSettingsDigest: digest, sequence, ...route };
+ return { linkId, lastApplyAt, runtimeStartedAt, appliedCodexSettingsDigest: digest, sequence };
}
export interface LinkChildParityStore {
@@ -115,10 +100,6 @@ export function createLinkChildParityStore(now: () => number = Date.now): LinkCh
lastApplyAt: report.lastApplyAt,
runtimeStartedAt: report.runtimeStartedAt,
appliedCodexSettingsDigest: report.appliedCodexSettingsDigest,
- ...(report.routeClientOutcomes === undefined ? {} : {
- appliedRouteIntentDigest: report.appliedRouteIntentDigest ?? null,
- routeClientOutcomes: routeClientOutcomesForPeer(report.routeClientOutcomes),
- }),
reportedAt: now(),
sequence: report.sequence,
});
@@ -128,7 +109,7 @@ export function createLinkChildParityStore(now: () => number = Date.now): LinkCh
const kept = reports.get(linkId);
if (!kept) return { ...EMPTY_LINK_CHILD_PARITY };
const { sequence: _sequence, ...parity } = kept;
- return { ...parity, ...(parity.routeClientOutcomes === undefined ? {} : { routeClientOutcomes: routeClientOutcomesForPeer(parity.routeClientOutcomes) }) };
+ return parity;
},
clear() { reports.clear(); },
};
diff --git a/src/link/management-routes.ts b/src/link/management-routes.ts
index c2b859f0a8c..98500320fca 100644
--- a/src/link/management-routes.ts
+++ b/src/link/management-routes.ts
@@ -16,8 +16,6 @@
export const LINK_MANAGEMENT_PREFIX = "/v1/link-management";
/** The Home's OpenCodex-owned Codex settings a Child applies when it syncs. */
export const LINK_CODEX_SETTINGS_PATH = `${LINK_MANAGEMENT_PREFIX}/codex-settings`;
-export const LINK_ROUTE_INTENT_PATH = `${LINK_MANAGEMENT_PREFIX}/route-intent`;
-export const LINK_ROUTE_CATALOG_PATH = `${LINK_MANAGEMENT_PREFIX}/route-catalog`;
export const LINK_MANAGEMENT_READ_PATHS: readonly string[] = [
"/api/system/health",
@@ -73,7 +71,7 @@ export function linkManagementTarget(url: URL): URL | null {
/** Home side: may the hub-link listener dispatch this request to the link management handler? */
export function linkManagementRouteAllowed(url: URL, req: Request): boolean {
if (req.headers.has("upgrade")) return false;
- if (url.pathname === LINK_CODEX_SETTINGS_PATH || url.pathname === LINK_ROUTE_INTENT_PATH || url.pathname === LINK_ROUTE_CATALOG_PATH) return req.method === "GET" && url.search === "";
+ if (url.pathname === LINK_CODEX_SETTINGS_PATH) return req.method === "GET" && url.search === "";
const target = linkManagementTarget(url);
return target !== null && linkManagementReadAllowed(req.method, target.pathname, target.searchParams);
}
diff --git a/src/link/remote-windows.ts b/src/link/remote-windows.ts
index b50281cb55c..ff6d84e6ed8 100644
--- a/src/link/remote-windows.ts
+++ b/src/link/remote-windows.ts
@@ -1,4 +1,3 @@
-import type { RouteClientId } from "./route-intent";
import { Buffer } from "node:buffer";
import { deflateRawSync, inflateRawSync } from "node:zlib";
@@ -211,27 +210,16 @@ Get-ChildItem -LiteralPath $dir -Filter '.ocx-child.purge.*.old' -Force -EA 0|%{
`);
}
-/**
- * The first OpenCodex release whose `ocx sync` understands --overwrite-conflict and
- * --overwrite-client; an earlier one ignores both and reports a plain sync as success. The
- * lightweight ocx-child supports neither and never reads route intent.
- */
-export const ROUTE_OVERWRITE_MIN_CHILD_VERSION = "2.100.0";
-
-export function windowsPinnedSyncArgv(linkId: string, overwriteConflict = false, overwriteClient?: RouteClientId): string[] {
- // Overwrite needs a Child that honours it: refuse ocx-child and older ocx before syncing.
- const minimum = overwriteConflict ? ROUTE_OVERWRITE_MIN_CHILD_VERSION.split(".").map(part => `'${part}'`).join(",") : "'2','93','0'";
- const lacks = overwriteConflict ? "ocx-fanout:child-ocx-lacks-overwrite" : "ocx-fanout:child-ocx-lacks-expect-link";
- const ephemeral = overwriteConflict ? String.raw`if(([IO.Path]::GetFileName($x)).ToLowerInvariant().StartsWith('ocx-child')){[Console]::Error.WriteLine('ocx-fanout:child-ephemeral-no-overwrite');exit 121};` : "";
- return packedPowerShellArgv(NATIVE_PROCESS + dataLine({ args: ["sync", "--expect-link", linkId, "--json", ...(overwriteConflict ? ["--overwrite-conflict"] : []), ...(overwriteClient ? ["--overwrite-client", overwriteClient] : [])] }) + OCX_CANDIDATES + String.raw`
+export function windowsPinnedSyncArgv(linkId: string): string[] {
+ return packedPowerShellArgv(NATIVE_PROCESS + dataLine({ args: ["sync", "--expect-link", linkId, "--json"] }) + OCX_CANDIDATES + String.raw`
foreach($x in ($c|select -Unique)){
- if(![IO.File]::Exists($x)){continue};` + ephemeral + String.raw`$v=[OcxJob]::V($x);if($null -eq $v){continue};$v=$v.Trim();if($v.StartsWith('opencodex ')){$v=$v.Substring(10)}
+ if(![IO.File]::Exists($x)){continue};$v=[OcxJob]::V($x);if($null -eq $v){continue};$v=$v.Trim();if($v.StartsWith('opencodex ')){$v=$v.Substring(10)}
$m=[regex]::Match($v,'^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(?:-([0-9A-Za-z.-]+))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$')
$bad=(!$m.Success -or $v.Length -gt 128);if(!$bad -and $m.Groups[4].Success){foreach($p in $m.Groups[4].Value.Split('.')){if(!$p -or ($p -match '^\d+$' -and $p.Length -gt 1 -and $p[0] -eq '0')){$bad=$true}}}
if($bad){[Console]::Error.WriteLine('ocx-fanout:child-ocx-version-unknown');exit 121}
- $a=@($m.Groups[1].Value,$m.Groups[2].Value,$m.Groups[3].Value);$b=@(` + minimum + String.raw`);$cmp=0
+ $a=@($m.Groups[1].Value,$m.Groups[2].Value,$m.Groups[3].Value);$b=@('2','93','0');$cmp=0
for($i=0;$i -lt 3;$i++){if($a[$i].Length -ne $b[$i].Length){$cmp=$a[$i].Length-$b[$i].Length;break};$cmp=[string]::CompareOrdinal($a[$i],$b[$i]);if($cmp -ne 0){break}}
- if($cmp -lt 0 -or ($cmp -eq 0 -and $m.Groups[4].Success)){[Console]::Error.WriteLine('` + lacks + String.raw`');exit 121}
+ if($cmp -lt 0 -or ($cmp -eq 0 -and $m.Groups[4].Success)){[Console]::Error.WriteLine('ocx-fanout:child-ocx-lacks-expect-link');exit 121}
[Console]::Error.WriteLine('ocx-fanout:selected:'+$v);[Console]::Out.WriteLine('ocx-fanout:stdout-begin');Invoke-Native $x @($d.args)
};exit 127
`);
diff --git a/src/link/route-intent-watch.ts b/src/link/route-intent-watch.ts
deleted file mode 100644
index 1f740c5cca6..00000000000
--- a/src/link/route-intent-watch.ts
+++ /dev/null
@@ -1,41 +0,0 @@
-/** Ephemeral Home wakeups. The admitted link id, never a caller selector, owns each slot. */
-export const ROUTE_INTENT_WATCH_HEADER = "x-ocx-route-intent-watch";
-export const ROUTE_INTENT_AFTER_HEADER = "x-ocx-route-intent-after";
-export const ROUTE_INTENT_WAKE_HEADER = "x-ocx-route-intent-wake";
-export const ROUTE_INTENT_WATCH_TIMEOUT_MS = 30_000;
-export const ROUTE_INTENT_WATCH_MAX_LINKS = 256;
-export type RouteIntentWake = "notified" | "timeout" | "cancelled" | "busy";
-
-export class RouteIntentWatch {
- private readonly waiters = new Map void>();
- constructor(private readonly timeoutMs = ROUTE_INTENT_WATCH_TIMEOUT_MS, private readonly maxLinks = ROUTE_INTENT_WATCH_MAX_LINKS) {
- if (!Number.isInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > ROUTE_INTENT_WATCH_TIMEOUT_MS
- || !Number.isInteger(maxLinks) || maxLinks < 1 || maxLinks > ROUTE_INTENT_WATCH_MAX_LINKS) throw new RangeError("invalid route intent watch bounds");
- }
- get size(): number { return this.waiters.size; }
- wait(linkId: string, signal: AbortSignal): Promise {
- if (signal.aborted) return Promise.resolve("cancelled");
- if (this.waiters.has(linkId) || this.waiters.size >= this.maxLinks) return Promise.resolve("busy");
- return new Promise(resolve => {
- const finish = (reason: RouteIntentWake) => {
- clearTimeout(timer);
- signal.removeEventListener("abort", abort);
- this.waiters.delete(linkId);
- resolve(reason);
- };
- const abort = () => finish("cancelled");
- const timer = setTimeout(() => finish("timeout"), this.timeoutMs);
- timer.unref();
- this.waiters.set(linkId, finish);
- signal.addEventListener("abort", abort, { once: true });
- if (signal.aborted) abort();
- });
- }
- /** False means no active watcher accepted the request; it is never an apply acknowledgment. */
- notify(linkId: string): boolean {
- const wake = this.waiters.get(linkId);
- wake?.("notified");
- return wake !== undefined;
- }
-}
-export const homeRouteIntentWatch = new RouteIntentWatch();
diff --git a/src/link/route-intent.ts b/src/link/route-intent.ts
deleted file mode 100644
index de1d578aedb..00000000000
--- a/src/link/route-intent.ts
+++ /dev/null
@@ -1,209 +0,0 @@
-/** Home policy only. No filesystem paths, listener facts, credentials or profile slots. */
-import { createHash } from "node:crypto";
-import { hostname } from "node:os";
-import { EXPORT_CLIENT_IDS } from "../clients/client-ids";
-import type { OcxConfig } from "../types";
-
-export const ROUTE_CLIENT_IDS = ["codex", "claude", "claude-desktop", "grok", ...EXPORT_CLIENT_IDS] as const;
-export type RouteClientId = (typeof ROUTE_CLIENT_IDS)[number];
-export interface RouteClientIntent {
- enabled: boolean;
- modelNames?: "alias" | "routed";
- /** Stable Aside user ids, never machine-local profile slots. */
- accounts?: "all" | "none" | string[];
-}
-export interface HomeRouteIntentDocument {
- version: 1;
- home: { name: string; version: string };
- clients: Record;
- /**
- * `native` appears only in the Home's own document: the Home's persisted native switch governs
- * that client there. Documents sent to a Child never carry it.
- */
- sources: Record;
- /** Device decisions (including inherit) invalidate matching enrollment overrides. */
- clientRevisions?: Partial>;
-}
-export interface RouteIntentState {
- version: 1;
- defaults: Partial>;
- devices: Record>>;
- revisions?: Record>>;
-}
-export interface RouteClientOutcome {
- client: RouteClientId;
- outcome: "current" | "stale" | "not_installed" | "conflict" | "failed";
- /** A Child's off-only opt-out; Home policy remains unchanged. */
- source?: "local";
- target?: string;
- message?: string;
- intentDigest?: string;
- /** Display-only initial selection made during enrollment; never mutates Home policy. */
- initialIntent?: RouteClientIntent;
- initialRevision?: number;
-}
-export const ROUTE_INTENT_DIGEST_PATTERN = /^[0-9a-f]{64}$/;
-export function isRouteClientId(value: unknown): value is RouteClientId {
- return typeof value === "string" && (ROUTE_CLIENT_IDS as readonly string[]).includes(value);
-}
-function record(value: unknown): value is Record {
- return value !== null && typeof value === "object" && !Array.isArray(value);
-}
-function keys(value: unknown, required: readonly string[], optional: readonly string[] = []): value is Record {
- return record(value) && required.every(k => Object.hasOwn(value, k))
- && Object.keys(value).every(k => required.includes(k) || optional.includes(k));
-}
-function label(value: unknown, max: number): value is string {
- return typeof value === "string" && value.length > 0 && value.length <= max && value === value.trim()
- && !/[\x00-\x1f\x7f\u202a-\u202e\u2066-\u2069]/u.test(value);
-}
-export function parseRouteClientIntent(value: unknown, client?: RouteClientId): RouteClientIntent | null {
- if (!keys(value, ["enabled"], ["modelNames", "accounts"]) || typeof value.enabled !== "boolean") return null;
- if (Object.hasOwn(value, "modelNames") && value.modelNames !== "alias" && value.modelNames !== "routed") return null;
- const a = value.accounts;
- if (Object.hasOwn(value, "accounts")) {
- if (client !== undefined && client !== "aside") return null;
- if (a !== "all" && a !== "none" && (!Array.isArray(a) || a.length > 128 || a.length === 0
- || !a.every(id => label(id, 128) && /^[A-Za-z0-9@._+-]+$/.test(id) && !/^u[0-9]+$/i.test(id))
- || new Set(a).size !== a.length)) return null;
- }
- return { enabled: value.enabled, ...(value.modelNames === undefined ? {} : { modelNames: value.modelNames as "alias" | "routed" }),
- ...(a === undefined ? {} : { accounts: Array.isArray(a) ? [...a].sort() : a as "all" | "none" }) };
-}
-export function emptyRouteIntentState(): RouteIntentState {
- return { version: 1, defaults: {}, devices: {} };
-}
-
-export interface HomeRouteProjectionOptions {
- /**
- * The native client a Home write is deciding right now. The decision is applied through the
- * Home's native switch; every other native client on the Home reads that switch as it is.
- */
- decidedClient?: RouteClientId;
- /** The decided Home value. Absent, the decided client takes the default Children inherit. */
- decidedIntent?: RouteClientIntent;
-}
-export const HOME_NATIVE_ROUTE_CLIENTS = ["codex", "claude", "grok", "claude-desktop"] as const;
-export type HomeNativeRouteClientId = (typeof HOME_NATIVE_ROUTE_CLIENTS)[number];
-export function isHomeNativeRouteClient(client: RouteClientId): client is HomeNativeRouteClientId {
- return (HOME_NATIVE_ROUTE_CLIENTS as readonly string[]).includes(client);
-}
-
-/**
- * The defaults Children inherit for the native clients, captured once from the Home's durable
- * switches. Codex and Claude Code keep their switch meaning (only an explicit `false` is off).
- * Grok and Claude Desktop are never captured on: a Home switch enables them on the Home alone.
- */
-export function homeNativeRouteDefaults(config: OcxConfig): Record {
- return { codex: { enabled: config.clientIntegrations?.codex !== false }, claude: { enabled: config.claudeCode?.enabled !== false },
- grok: { enabled: false }, "claude-desktop": { enabled: false } };
-}
-
-/**
- * Pure projection. Persisted policy is supplied explicitly by the management owner.
- *
- * On the Home itself (`deviceId === "home"`) the native clients follow the Home's own persisted
- * switches with their usual meaning (only an explicit `false` is off), never captured defaults or
- * a `devices.home` entry: ocx restore, Messages off and native toggles change those switches
- * without recording route intent. Children never read those switches: a device override, then the
- * saved default, then the builtin (Codex and Claude Code on, everything else off).
- */
-export function projectHomeRouteIntent(
- config: OcxConfig, version: string, deviceId?: string, state: RouteIntentState = emptyRouteIntentState(),
- options: HomeRouteProjectionOptions = {},
-): HomeRouteIntentDocument {
- const clients = {} as HomeRouteIntentDocument["clients"];
- const sources = {} as HomeRouteIntentDocument["sources"];
- for (const client of ROUTE_CLIENT_IDS) {
- const home = state.defaults[client];
- if (deviceId === "home" && isHomeNativeRouteClient(client)) {
- const live = client === "claude" ? config.claudeCode?.enabled : config.clientIntegrations?.[client];
- const decided = options.decidedClient === client;
- const enabled = decided ? options.decidedIntent?.enabled ?? home?.enabled ?? (client === "codex" || client === "claude")
- : live !== false;
- clients[client] = { enabled };
- sources[client] = "native";
- continue;
- }
- const device = deviceId === undefined ? undefined : state.devices[deviceId]?.[client];
- const chosen = { enabled: client === "codex" || client === "claude", ...home, ...device };
- clients[client] = { ...chosen, ...(Array.isArray(chosen.accounts) ? { accounts: [...chosen.accounts].sort() } : {}) };
- sources[client] = device ? "device" : home ? "default" : "builtin";
- }
- const name = hostname();
- const revisions = deviceId === undefined ? undefined : state.revisions?.[deviceId];
- return { version: 1, home: { name: label(name, 255) ? name : "Home", version }, clients, sources,
- ...(revisions && Object.keys(revisions).length ? { clientRevisions: { ...revisions } } : {}) };
-}
-/** `allowNative` is for the Home's own local receipt; a document from a Home never carries it. */
-export function parseHomeRouteIntentDocument(value: unknown, options: { allowNative?: boolean } = {}): HomeRouteIntentDocument | null {
- if (!keys(value, ["version", "home", "clients", "sources"], ["clientRevisions"]) || value.version !== 1
- || !keys(value.home, ["name", "version"]) || !label(value.home.name, 255) || !label(value.home.version, 64)
- || !keys(value.clients, ROUTE_CLIENT_IDS) || !keys(value.sources, ROUTE_CLIENT_IDS)) return null;
- const clients = {} as HomeRouteIntentDocument["clients"];
- const sources = {} as HomeRouteIntentDocument["sources"];
- for (const client of ROUTE_CLIENT_IDS) {
- const intent = parseRouteClientIntent(value.clients[client], client);
- const source = value.sources[client];
- if (!intent || (source !== "builtin" && source !== "default" && source !== "device"
- && !(source === "native" && options.allowNative === true && isHomeNativeRouteClient(client)))) return null;
- clients[client] = intent;
- sources[client] = source;
- }
- const revisions: Partial> = {};
- if (value.clientRevisions !== undefined) {
- if (!record(value.clientRevisions)) return null;
- for (const [client, revision] of Object.entries(value.clientRevisions)) {
- if (!isRouteClientId(client) || typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 0) return null;
- revisions[client] = revision;
- }
- }
- return { version: 1, home: { name: value.home.name, version: value.home.version }, clients, sources,
- ...(value.clientRevisions === undefined ? {} : { clientRevisions: revisions }) };
-}
-/** Compact JSON: version, ordered clients (enabled/modelNames/accounts), ordered sources. */
-export function routeIntentDigestPayload(document: HomeRouteIntentDocument): string {
- const clients = Object.fromEntries(ROUTE_CLIENT_IDS.map(client => {
- const intent = document.clients[client];
- return [client, { enabled: intent.enabled, ...(intent.modelNames === undefined ? {} : { modelNames: intent.modelNames }),
- ...(intent.accounts === undefined ? {} : { accounts: Array.isArray(intent.accounts) ? [...intent.accounts].sort() : intent.accounts }) }];
- }));
- return JSON.stringify({ version: 1, clients, sources: Object.fromEntries(ROUTE_CLIENT_IDS.map(client => [client, document.sources[client]])) });
-}
-export function routeIntentDigest(document: HomeRouteIntentDocument): string {
- return createHash("sha256").update(routeIntentDigestPayload(document), "utf8").digest("hex");
-}
-
-export function routeClientIntentDigest(document: HomeRouteIntentDocument, client: RouteClientId): string {
- const intent = parseRouteClientIntent(document.clients[client], client)!;
- return createHash("sha256").update(JSON.stringify({ version: 1, client, intent, source: document.sources[client] }), "utf8").digest("hex");
-}
-
-/** Peer outcomes carry closed fields; local diagnostics are never copied onto the wire. */
-export function parseRouteClientOutcomes(value: unknown): RouteClientOutcome[] | null {
- if (!Array.isArray(value) || value.length > ROUTE_CLIENT_IDS.length) return null;
- const seen = new Set();
- const out: RouteClientOutcome[] = [];
- for (const row of value) {
- if (!keys(row, ["client", "outcome"], ["source", "target", "message", "intentDigest", "initialIntent", "initialRevision"]) || !isRouteClientId(row.client)
- || seen.has(row.client) || !["current", "stale", "not_installed", "conflict", "failed"].includes(row.outcome as string)
- || (row.source !== undefined && row.source !== "local")
- || (row.target !== undefined && !label(row.target, 256)) || (row.message !== undefined && !label(row.message, 256))) return null;
- if (row.intentDigest !== undefined && (typeof row.intentDigest !== "string" || !ROUTE_INTENT_DIGEST_PATTERN.test(row.intentDigest))) return null;
- const initialIntent = row.initialIntent === undefined ? undefined : parseRouteClientIntent(row.initialIntent, row.client);
- if (row.initialIntent !== undefined && (!initialIntent || Object.keys(initialIntent).some(key => key !== "enabled") || typeof row.initialRevision !== "number" || !Number.isSafeInteger(row.initialRevision) || row.initialRevision < 0)) return null;
- if (row.initialIntent === undefined && row.initialRevision !== undefined) return null;
- seen.add(row.client);
- out.push({ client: row.client, outcome: row.outcome as RouteClientOutcome["outcome"], ...(row.source === "local" ? { source: "local" as const } : {}),
- ...(row.target === undefined ? {} : { target: row.target as string }), ...(row.message === undefined ? {} : { message: row.message as string }),
- ...(row.intentDigest === undefined ? {} : { intentDigest: row.intentDigest }),
- ...(initialIntent ? { initialIntent, initialRevision: row.initialRevision as number } : {}) });
- }
- return out;
-}
-/** Publish only client identity, disposition and opt-out; target/message can expose local files or secrets. */
-export function routeClientOutcomesForPeer(outcomes: readonly RouteClientOutcome[]): RouteClientOutcome[] {
- return outcomes.map(({ client, outcome, source, target, intentDigest, initialIntent, initialRevision }) => ({ client, outcome, ...(source === "local" ? { source } : {}),
- ...(intentDigest ? { intentDigest } : {}), ...(initialIntent ? { initialIntent: { enabled: initialIntent.enabled }, initialRevision } : {}),
- ...(client === "aside" && typeof target === "string" && /^u[0-9]+(?:,u[0-9]+)*$/.test(target) && target.length <= 256 ? { target } : {}) }));
-}
diff --git a/src/link/ssh-argv.ts b/src/link/ssh-argv.ts
index c471c495e8c..5647d6bb168 100644
--- a/src/link/ssh-argv.ts
+++ b/src/link/ssh-argv.ts
@@ -1,4 +1,3 @@
-import { isRouteClientId, type RouteClientId } from "./route-intent";
/**
* Argument vectors for the system OpenSSH client used by machine links.
*
@@ -20,7 +19,7 @@ import { isRouteClientId, type RouteClientId } from "./route-intent";
*/
import { isAbsolute } from "node:path";
-import { POWERSHELL_ENCODED_MAX, ROUTE_OVERWRITE_MIN_CHILD_VERSION, windowsOcxArgv, windowsPinnedSyncArgv } from "./remote-windows";
+import { POWERSHELL_ENCODED_MAX, windowsOcxArgv, windowsPinnedSyncArgv } from "./remote-windows";
export type TunnelDirection = "R" | "L";
@@ -304,26 +303,6 @@ try_ocx() {
${REMOTE_OCX_CANDIDATES}
exit ${REMOTE_COMMAND_NOT_FOUND}`;
-export { ROUTE_OVERWRITE_MIN_CHILD_VERSION };
-
-function replaceOnce(script: string, from: string, to: string): string {
- if (!script.includes(from)) throw new Error(`pinned sync script lost its anchor: ${from}`);
- return script.replace(from, () => to);
-}
-
-/** Overwrite needs a Child that honours it: refuse ocx-child and older ocx before syncing. */
-function overwritePinnedSyncScript(overwriteClient?: RouteClientId): string {
- const [major, minor, patch] = ROUTE_OVERWRITE_MIN_CHILD_VERSION.split(".");
- let script = replaceOnce(REMOTE_PINNED_SYNC_SCRIPT, 'c=cmp(a[1],"2"); if(!c)c=cmp(a[2],"93"); if(!c)c=cmp(a[3],"0");',
- `c=cmp(a[1],"${major}"); if(!c)c=cmp(a[2],"${minor}"); if(!c)c=cmp(a[3],"${patch}");`);
- script = replaceOnce(script, "'ocx-fanout:child-ocx-lacks-expect-link'", "'ocx-fanout:child-ocx-lacks-overwrite'");
- script = replaceOnce(script, ' [ -x "$c" ] || return\n',
- ' [ -x "$c" ] || return\n case "$c" in */ocx-child) printf \'%s\\n\' \'ocx-fanout:child-ephemeral-no-overwrite\' >&2; exit 121;; esac\n');
- return replaceOnce(script, 'sync --expect-link "$1" --json', 'sync --expect-link "$1" --json --overwrite-conflict' + (overwriteClient ? ` --overwrite-client ${overwriteClient}` : ''));
-}
-
-export function remotePinnedSyncArgv(linkId: string, platform: "posix" | "windows" = "posix", overwriteConflict = false, overwriteClient?: RouteClientId): string[] {
- if (overwriteClient !== undefined && (!overwriteConflict || !isRouteClientId(overwriteClient))) throw new LinkSshArgumentError("invalid overwrite client");
- const script = overwriteConflict ? overwritePinnedSyncScript(overwriteClient) : REMOTE_PINNED_SYNC_SCRIPT;
- return platform === "windows" ? windowsPinnedSyncArgv(linkId, overwriteConflict, overwriteClient) : ["sh", "-c", script, "ocx", linkId];
+export function remotePinnedSyncArgv(linkId: string, platform: "posix" | "windows" = "posix"): string[] {
+ return platform === "windows" ? windowsPinnedSyncArgv(linkId) : ["sh", "-c", REMOTE_PINNED_SYNC_SCRIPT, "ocx", linkId];
}
diff --git a/src/providers/github-copilot-auto.ts b/src/providers/github-copilot-auto.ts
index e42347342be..29797761aa6 100644
--- a/src/providers/github-copilot-auto.ts
+++ b/src/providers/github-copilot-auto.ts
@@ -130,32 +130,15 @@ export async function resolveCopilotAuto(provider: OcxProviderConfig, requestedM
let models: CopilotModel[];
try { models = await fetchCopilotAutoModels(transport, signal, beforeSend, 60_000, concurrency); }
catch (error) {
- if (signal?.aborted) throw error;
- // A manual account switch makes `beforeSend` return false, which cancels this
- // control-plane send. That is not a client abort: fall back to the requested model
- // so the dispatch path revalidates the selection and rebuilds on the new account.
- if (error instanceof ProviderOutboundSendCancelledError) {
- return { provider: transport, modelId: requestedModel, auto: false };
- }
+ if (signal?.aborted || error instanceof ProviderOutboundSendCancelledError) throw error;
// Permission discovery is additive for legacy paid configurations. A temporary
// discovery outage must not turn a previously usable named route into a refusal.
return { provider: transport, modelId: requestedModel, auto: false };
}
if (!copilotRequiresAuto(provider, models))
return { provider: transport, modelId: requestedModel, auto: false };
- // The same authority fence guards the later control-plane sends: a manual switch that
- // lands during the session or intent step falls back too, so dispatch rebuilds on the
- // new account instead of failing the whole negotiation. A client abort still rejects.
- const fallBackOnSwitch = (error: unknown) => {
- if (!signal?.aborted && error instanceof ProviderOutboundSendCancelledError)
- return { provider: transport, modelId: requestedModel, auto: false } as const;
- throw error;
- };
- let session: Awaited>;
- try {
- session = await requestJson(transport, "/models/session", signal,
- { auto_mode: { model_hints: ["auto"] } }, undefined, beforeSend, concurrency);
- } catch (error) { return fallBackOnSwitch(error); }
+ const session = await requestJson(transport, "/models/session", signal,
+ { auto_mode: { model_hints: ["auto"] } }, undefined, beforeSend, concurrency);
const pool = strings(session.available_models);
if (typeof session.session_token !== "string" || !session.session_token || session.session_token.length > 32_768 || !/^[\x21-\x7e]+$/.test(session.session_token) || !pool.length)
throw new Error("GitHub Copilot Auto session omitted its routing credentials");
@@ -170,11 +153,8 @@ export async function resolveCopilotAuto(provider: OcxProviderConfig, requestedM
? typeof lastUser.content === "string" ? lastUser.content : lastUser.content.flatMap(/** Extract text parts for intent negotiation without forwarding image or other multimodal payloads. */
part => part.type === "text" ? [part.text] : []).join("\n")
: "";
- let intent: Awaited>;
- try {
- intent = await requestJson(transport, "/models/session/intent", signal,
- { prompt: prompt.slice(0, 32_768), available_models: pool }, session.session_token, beforeSend, concurrency);
- } catch (error) { return fallBackOnSwitch(error); }
+ const intent = await requestJson(transport, "/models/session/intent", signal,
+ { prompt: prompt.slice(0, 32_768), available_models: pool }, session.session_token, beforeSend, concurrency);
const modelId = strings(intent.candidate_models).find(/** Choose the first server-ranked candidate that belongs to this credential-bound session pool. */
candidate => pool.includes(candidate));
if (!modelId) throw new Error("GitHub Copilot Auto intent returned no eligible model");
diff --git a/src/providers/model-rename-fields.ts b/src/providers/model-rename-fields.ts
index 62dde304cd3..a97202d8bdc 100644
--- a/src/providers/model-rename-fields.ts
+++ b/src/providers/model-rename-fields.ts
@@ -103,8 +103,6 @@ export const PROVIDER_MODEL_RENAME_ROLES = {
modelSupportsVerbosity: "record",
supportsVerbosity: "none",
modelReasoningSummaryDelivery: "record",
- thinkingDisplay: "none",
- modelThinkingDisplay: "record",
modelPreferHostedTools: "record",
supportsOpenAiWebSearchToolFields: "none",
xaiResponsesXSearch: "none",
diff --git a/src/providers/quota/vendor-probes-key.ts b/src/providers/quota/vendor-probes-key.ts
index 9a258c0628d..e34d581d787 100644
--- a/src/providers/quota/vendor-probes-key.ts
+++ b/src/providers/quota/vendor-probes-key.ts
@@ -24,13 +24,7 @@ async function quotaFetch(providerName: string, config: OcxProviderConfig, url:
}
const KIMI_CODE_BASE_URL = "https://api.kimi.com/coding/v1";
-/**
- * Kimi's regional (kimi.ai) Coding API. Its `/usages` answers a Coding Plan API key the same way
- * the kimi.com origin does (#8332). Only key auth may use it: the probe then calls the origin that
- * already receives this key for inference, so it adds no credential destination. OAuth tokens
- * stay on the kimi.com origin they were issued for.
- */
-const KIMI_CODE_REGIONAL_BASE_URL = "https://api.kimi.ai/coding/v1";
+const KIMI_CODE_USAGE_URL = `${KIMI_CODE_BASE_URL}/usages`;
const COMMAND_CODE_BASE_URL = "https://api.commandcode.ai";
const COMMAND_CODE_WHOAMI_URL = `${COMMAND_CODE_BASE_URL}/alpha/whoami`;
const COMMAND_CODE_CREDITS_URL = `${COMMAND_CODE_BASE_URL}/alpha/billing/credits`;
@@ -1125,18 +1119,6 @@ export function isCanonicalKimiCodeBaseUrl(baseUrl: string): boolean {
return normalizedBaseUrl(baseUrl) === KIMI_CODE_BASE_URL;
}
-/**
- * The Kimi Coding origin whose `/usages` a quota probe may call with this provider's bearer, or
- * null. It is always the configured inference origin, matched exactly against Kimi's own origins,
- * so a user-edited or lookalike host never receives the credential.
- */
-function kimiCodeQuotaBaseUrl(config: Pick): string | null {
- const normalized = normalizedBaseUrl(config.baseUrl);
- if (normalized === KIMI_CODE_BASE_URL) return normalized;
- if (normalized === KIMI_CODE_REGIONAL_BASE_URL && (config.authMode ?? "key") === "key") return normalized;
- return null;
-}
-
export function isCanonicalCommandCodeBaseUrl(baseUrl: string): boolean {
const normalized = normalizedBaseUrl(baseUrl);
// OAuth preset points at the API root; the Provider-API preset at /provider/v1.
@@ -1288,10 +1270,9 @@ async function resolveKimiQuotaBearer(config: OcxProviderConfig, accountId?: str
export async function fetchKimiQuota(provider: string, config: OcxProviderConfig, accessToken: string): Promise {
// Never release credentials to a user-edited or lookalike provider host.
- const baseUrl = kimiCodeQuotaBaseUrl(config);
- if (!baseUrl) return null;
+ if (!isCanonicalKimiCodeBaseUrl(config.baseUrl)) return null;
if (!accessToken) return null;
- const response = await quotaFetch(provider, config, `${baseUrl}/usages`, {
+ const response = await quotaFetch(provider, config, KIMI_CODE_USAGE_URL, {
headers: { Accept: "application/json", Authorization: `Bearer ${accessToken}` },
redirect: "error",
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
@@ -1454,7 +1435,7 @@ type KeyQuotaReader = (name: string, provider: OcxProviderConfig) => Promise {
const bearer = await resolveKimiQuotaBearer(config);
return bearer ? fetchKimiQuota(id, config, bearer) : null;
diff --git a/src/providers/resolved-model-policy.ts b/src/providers/resolved-model-policy.ts
index fa3e6f6b29a..bacdb6dfb97 100644
--- a/src/providers/resolved-model-policy.ts
+++ b/src/providers/resolved-model-policy.ts
@@ -43,7 +43,6 @@ export type StaticProviderPolicyField =
| "reasoningDetailsModels" | "thinkingToggleModels" | "thinkingBudgetModels" | "showThinkingSummary"
| "escapeBuiltinToolNames" | "googleMode" | "project" | "location" | "modelCapabilities"
| "modelAutoCompactTokenLimits" | "modelSuppressSyntheticMax" | "modelReasoningSummaryDelivery"
- | "thinkingDisplay" | "modelThinkingDisplay"
| "codexToolMode" | "modelAdapters";
type StaticProviderPolicyShape = Pick;
export type ResolvedProviderStaticPolicy = Readonly> & Readonly<{
@@ -183,8 +182,6 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
"supportsVerbosity", "responsesItemIdRepair",
"showThinkingSummary", "escapeBuiltinToolNames", "googleMode", "project", "location",
] as const) putScalar(key, entry?.[key] as StaticProviderPolicyShape[typeof key] | undefined);
- // Operator-only display policy; no registry entry declares it (#8363).
- put("thinkingDisplay", provider.thinkingDisplay, provider.thinkingDisplay !== undefined ? "operator" : "unknown");
const legacyClinePassLadder = entry !== undefined && input.providerName === "cline-pass"
&& provider.reasoningWireFormat === "gateway-object"
&& sameStringArray(provider.reasoningEfforts, ["low"]);
@@ -221,7 +218,6 @@ export function resolveModelPolicy(input: ResolveModelPolicyInput): ResolvedMode
for (const key of [
"modelAutoCompactTokenLimits", "modelSuppressSyntheticMax",
"modelReasoningSummaryDelivery", "codexToolMode", "modelAdapters",
- "modelThinkingDisplay",
] as const) putScalar(key, undefined);
put("modelCapabilities", mergeReasoningModelCapabilities(entry?.modelCapabilities, provider.modelCapabilities),
provider.modelCapabilities !== undefined ? "operator-capability" : entry?.modelCapabilities !== undefined ? "registry" : "unknown");
diff --git a/src/routing/home-route-capture.ts b/src/routing/home-route-capture.ts
deleted file mode 100644
index 55ef3ad0684..00000000000
--- a/src/routing/home-route-capture.ts
+++ /dev/null
@@ -1,123 +0,0 @@
-/**
- * Children inherit the Home's native switches as they stood when route intent began, never as they
- * are now: ocx restore, Messages off, Claude Desktop apply and the Claude toggle are Home-only.
- *
- * One idempotent capture freezes the four native defaults into route-intent.json. It runs before
- * a Home default write and at Child enrollment, and, only once the Home has a link, when its link
- * listener binds (a failure there is retried, see link-listener.ts) and before any Home-only
- * decision changes a switch. On a Home with no link Home-only decisions leave route-intent.json
- * alone, so a Child enrolled later inherits the switches as they stand at enrollment unless a Home
- * default write already saved them. Reads never capture. Once a default is saved it is never
- * captured again; only a Home default write changes it.
- */
-import { readConfigDiagnostics } from "../config";
-import { withConfigMutationLockSync } from "../config/mutation-lock";
-import { setIntegrationEnabled, type CodexDesiredStateResult, type DurableIntentClientId } from "../codex/desired-state";
-import { linkStorePath } from "../link/paths";
-import { readLinkStore } from "../link/store";
-import { HOME_NATIVE_ROUTE_CLIENTS, homeNativeRouteDefaults, type HomeNativeRouteClientId, type RouteClientIntent } from "../link/route-intent";
-import type { OcxConfig } from "../types";
-import { readRouteIntentState, routeIntentStatePath, updateRouteIntentState, type RouteIntentState } from "./route-intent-state";
-
-function captured(state: RouteIntentState): boolean {
- return HOME_NATIVE_ROUTE_CLIENTS.every(client => state.defaults[client] !== undefined);
-}
-
-/**
- * Whether a linked Child inherits these defaults. An ephemeral `ocx-child` never reads route intent,
- * so a Home whose only links are ephemeral has nothing to protect. Unreadable trust metadata counts
- * as linked: a Child may still depend on these defaults.
- */
-export function homeHasLinks(): boolean {
- try { return readLinkStore(linkStorePath()).links.some(link => link.childKind !== "ephemeral"); } catch { return true; }
-}
-
-export interface CaptureHomeRouteDefaultsOptions {
- /**
- * Every Home-only switch writer passes this: a Home without any link has no Child to protect
- * yet, so route-intent.json is neither read nor written for it.
- */
- whenLinked?: boolean;
-}
-
-/** The native defaults the Home's durable switches give (`fallback` only when the file is unreadable); null on a Child. */
-export function durableHomeNativeRouteDefaults(fallback?: OcxConfig): Record | null {
- const latest = readConfigDiagnostics();
- const durable = latest.source === "file" && !latest.error ? latest.config : fallback;
- return !durable || durable.runtimeRole === "client" ? null : homeNativeRouteDefaults(durable);
-}
-
-/** Fills only the missing native defaults; a saved one is never captured again. */
-export function fillHomeNativeRouteDefaults(state: RouteIntentState, defaults: Record): boolean {
- let filled = false;
- for (const client of HOME_NATIVE_ROUTE_CLIENTS) {
- if (state.defaults[client] !== undefined) continue;
- state.defaults[client] = defaults[client];
- filled = true;
- }
- return filled;
-}
-
-/**
- * Saves the missing native defaults from the durable switches (`fallback` only when the config
- * file is unreadable). A Child never captures. Returns whether anything was saved.
- */
-export function captureHomeRouteDefaults(fallback?: OcxConfig, options: CaptureHomeRouteDefaultsOptions = {}): boolean {
- if (fallback?.runtimeRole === "client") return false;
- if (options.whenLinked && !homeHasLinks()) return false;
- if (captured(readRouteIntentState())) return false;
- return withConfigMutationLockSync(() => {
- const defaults = durableHomeNativeRouteDefaults(fallback);
- if (!defaults) return false;
- let saved = false;
- updateRouteIntentState(state => { saved = fillHomeNativeRouteDefaults(state, defaults); return state; });
- return saved;
- });
-}
-
-/**
- * A Home-only switch write refused because the Children's defaults could not be saved first.
- * Writing the switch anyway would change what Children inherit once the file is repaired.
- */
-export interface HomeRouteCaptureRefusal {
- readonly ok: false;
- readonly reason: "unavailable";
- readonly retryable: boolean;
- readonly message: string;
-}
-
-export type HomeIntegrationResult = CodexDesiredStateResult | HomeRouteCaptureRefusal;
-
-/** Only SQLITE_BUSY is contention worth retrying; any other lock failure repeats forever. */
-function lockContention(error: unknown): boolean {
- if (!error || typeof error !== "object") return false;
- return (error as { code?: unknown }).code === "CONFIG_MUTATION_LOCK_UNAVAILABLE"
- && (error as { cause?: { code?: unknown } }).cause?.code === "SQLITE_BUSY";
-}
-
-export function captureRefusal(error: unknown): HomeRouteCaptureRefusal {
- const path = routeIntentStatePath();
- if (lockContention(error)) return { ok: false, reason: "unavailable", retryable: true,
- message: `Another process is saving the configuration, so the Children's route defaults in ${path} could not be saved first; nothing was changed. Retry in a moment.` };
- return { ok: false, reason: "unavailable", retryable: false,
- message: `The Children's route defaults in ${path} could not be read or saved; nothing was changed. Inspect it, fix or remove it, then retry.` };
-}
-
-/** `captureHomeRouteDefaults` for the Home-only switch writers: a failure is a typed refusal, never a throw. */
-export function captureHomeRouteDefaultsBeforeHomeSwitch(): HomeRouteCaptureRefusal | null {
- try {
- captureHomeRouteDefaults(undefined, { whenLinked: true });
- return null;
- } catch (error) {
- return captureRefusal(error);
- }
-}
-
-/**
- * The Home-only switch writers (ocx restore, ocx restore back, Claude Desktop apply from the CLI
- * and the dashboard) go through here. desired-state.ts itself stays free of link state: it is on
- * the always-on request path (tests/lab/core-link-boundary.test.ts).
- */
-export function setHomeIntegrationEnabled(client: DurableIntentClientId, enabled: boolean): HomeIntegrationResult {
- return captureHomeRouteDefaultsBeforeHomeSwitch() ?? setIntegrationEnabled(client, enabled);
-}
diff --git a/src/routing/home-route-intent.ts b/src/routing/home-route-intent.ts
deleted file mode 100644
index 32ae8226567..00000000000
--- a/src/routing/home-route-intent.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/** Upgrade bridge: existing journaled integrations are Home intent, never unowned file guesses. */
-import { homedir } from "node:os";
-import { join, resolve } from "node:path";
-import { readBoundedRegularFileSync } from "../lib/bounded-file-read";
-import { createIntegrationStateStore, type IntegrationStateStore } from "../integrations/store";
-import { listAsideProfileStores } from "../integrations/aside-profile-context";
-import { asideHomeDir } from "../clients/config-export";
-import { parseRouteClientIntent, projectHomeRouteIntent, ROUTE_CLIENT_IDS, type HomeRouteIntentDocument, type RouteClientId, type RouteClientIntent, type RouteIntentState } from "../link/route-intent";
-import { dropHomeNativeOverrides, homeRouteIntentInUse, readRouteIntentState, setRouteClientIntent, updateRouteIntentState } from "./route-intent-state";
-import { captureHomeRouteDefaults, durableHomeNativeRouteDefaults, fillHomeNativeRouteDefaults, homeHasLinks, type CaptureHomeRouteDefaultsOptions } from "./home-route-capture";
-import { withConfigMutationLockSync } from "../config/mutation-lock";
-import { readConfigDiagnostics } from "../config";
-import { packageVersion } from "../lib/package-version";
-import type { OcxConfig } from "../types";
-
-/** Existing Aside ownership must be resolved explicitly, never projected as off. */
-export class HomeAsideIntentDerivationError extends Error {
- constructor(cause: unknown) {
- super("Existing Aside ownership needs an explicit Home route intent", { cause });
- this.name = "HomeAsideIntentDerivationError";
- }
-}
-
-export function readHomeRouteIntentState(config: OcxConfig, options: { store?: IntegrationStateStore; home?: string; state?: RouteIntentState } = {}): RouteIntentState {
- const state = structuredClone(options.state ?? readRouteIntentState());
- const store = options.store ?? createIntegrationStateStore();
- const records = store.readRecordsStrict();
- for (const [client, owner] of Object.entries(records)) {
- if (state.defaults[client as keyof typeof state.defaults] !== undefined || !owner || client === "aside") continue;
- const intent = parseRouteClientIntent({ enabled: true, ...(owner.modelNames ? { modelNames: owner.modelNames } : {}) });
- if (intent) state.defaults[client as keyof typeof state.defaults] = intent;
- }
- if (state.defaults.aside !== undefined) return state;
- const aside = deriveHomeAsideIntent(config, { store, home: options.home });
- if (aside) state.defaults.aside = aside;
- return state;
-}
-
-/**
- * The Aside intent the Home's current Aside ownership amounts to: the stable userIds of every owned
- * profile, all profiles under the all-profiles policy, or undefined when nothing is owned.
- * `extraProfileIds` adds profiles about to be enabled, so a caller can refuse before writing
- * anything when one of them has no stable userId. Throws HomeAsideIntentDerivationError.
- */
-export function deriveHomeAsideIntent(config: OcxConfig, options: { store?: IntegrationStateStore; home?: string; extraProfileIds?: readonly number[] | "all" } = {}): RouteClientIntent | undefined {
- const store = options.store ?? createIntegrationStateStore();
- const records = store.readRecordsStrict();
- const scopes = listAsideProfileStores(store).filter(scope => scope.store.readRecordsStrict().aside);
- const extra = options.extraProfileIds ?? [];
- if (!records.aside && scopes.length === 0 && extra.length === 0 && config.asideProfileSync?.allProfiles !== true) return undefined;
- if (config.asideProfileSync?.allProfiles === true) return { enabled: true, accounts: "all" };
- try {
- const raw: unknown = JSON.parse(readBoundedRegularFileSync(join(asideHomeDir(process.env, options.home ?? homedir()), "accounts.json"), 4 * 1024 * 1024).toString("utf8"));
- if (!raw || typeof raw !== "object" || !("accounts" in raw) || !Array.isArray(raw.accounts)) throw new Error("existing Aside selection needs explicit route intent");
- const every = extra === "all" ? raw.accounts.flatMap((row: unknown) => row && typeof row === "object" && "id" in row && typeof row.id === "number" ? [row.id] : []) : extra;
- const slots = new Set([...scopes.map(scope => scope.profileId), ...every]);
- if (records.aside) {
- const root = asideHomeDir(process.env, options.home ?? homedir());
- const owned = resolve(records.aside.configPath);
- const matches = raw.accounts.filter((row: unknown) => row && typeof row === "object" && "id" in row
- && typeof row.id === "number" && Number.isSafeInteger(row.id) && row.id >= 0
- && resolve(join(root, "u", String(row.id), "models.json")) === owned);
- if (matches.length !== 1) throw new Error("existing Aside owner needs explicit route intent");
- slots.add(matches[0].id);
- }
- const userIds: string[] = [];
- for (const slot of slots) {
- const row = raw.accounts.find((value: unknown) => value && typeof value === "object" && "id" in value && value.id === slot);
- if (!row || typeof row !== "object" || !("userId" in row) || typeof row.userId !== "string") throw new Error("existing Aside selection needs explicit stable userIds");
- userIds.push(row.userId);
- }
- const intent = parseRouteClientIntent({ enabled: true, accounts: [...new Set(userIds)] }, "aside");
- if (!intent) throw new Error("existing Aside selection needs explicit stable userIds");
- return intent;
- } catch (cause) {
- throw new HomeAsideIntentDerivationError(cause);
- }
-}
-
-/**
- * Records a Home Aside profile change (the dashboard, `ocx integration client enable --client aside
- * --profile N`, an Aside restore) as the Home's `devices.home.aside` decision, naming the stable
- * userIds of every profile it owns afterwards, so ocx sync keeps it. Only once the Home uses route
- * intent; returns whether a decision was recorded. Throws HomeAsideIntentDerivationError.
- */
-export function recordHomeAsideDecision(config: OcxConfig): boolean {
- return withConfigMutationLockSync(() => {
- if (!homeRouteIntentInUse(readRouteIntentState())) return false;
- setRouteClientIntent("aside", deriveHomeAsideIntent(config) ?? { enabled: false }, "home");
- return true;
- });
-}
-
-/**
- * Capture defaults before a local materializer changes native switches or owners. The native
- * defaults come from the shared one-time capture; every other client from ownership or builtin.
- * A Home-only decision passes `whenLinked`, the rule every Home-only switch writer follows: a Home
- * without links has no Child to protect, so nothing reads or writes route-intent.json for it. Saving
- * ownership defaults there would pin clients the dashboard later turns on (ocx sync applies saved
- * defaults), and a damaged file would block a toggle that never needed it.
- */
-export function initializeHomeRouteIntentDefaults(config: OcxConfig, version: string, decidingDefault?: RouteClientId,
- options: CaptureHomeRouteDefaultsOptions = {}): void {
- if (options.whenLinked && !homeHasLinks()) return;
- withConfigMutationLockSync(() => {
- // Every default is derived before anything is written: an Aside refusal leaves the file as it was.
- const native = durableHomeNativeRouteDefaults(config);
- const prior = readRouteIntentState();
- if (native) fillHomeNativeRouteDefaults(prior, native);
- if (decidingDefault) prior.defaults[decidingDefault] = { enabled: false };
- const inherited = projectHomeRouteIntent(config, version, undefined, readHomeRouteIntentState(config, { state: prior }));
- updateRouteIntentState(state => {
- if (native) fillHomeNativeRouteDefaults(state, native);
- for (const [client, intent] of Object.entries(inherited.clients)) state.defaults[client as keyof typeof state.defaults] ??= intent;
- // A `devices.home` native entry from an earlier version no longer decides anything.
- dropHomeNativeOverrides(state);
- return state;
- });
- });
-}
-
-/**
- * The link listener's capture of every default a Child inherits, from the Home's durable config:
- * the native switches and the file clients' ownership, frozen in one write. An upgraded Home that
- * saved only the native defaults gets the rest here, so a later dashboard toggle cannot move a
- * Child's default. When existing Aside ownership cannot be resolved nothing else is saved; the
- * native defaults are then captured on their own and the error is raised so the listener retries.
- */
-export function captureChildRouteDefaults(): void {
- const latest = readConfigDiagnostics();
- if (latest.error) throw new Error("the configuration could not be read");
- if (latest.config.runtimeRole === "client") return;
- try {
- initializeHomeRouteIntentDefaults(latest.config, packageVersion("0.0.0"), undefined, { whenLinked: true });
- } catch (error) {
- if (error instanceof HomeAsideIntentDerivationError) captureHomeRouteDefaults(undefined, { whenLinked: true });
- throw error;
- }
-}
-
-/**
- * What a Child inherits. Never reads the Home's live native switches; see home-route-capture.
- * A pure read: enrollment and the link listener's start have already captured the native defaults.
- */
-export function projectChildRouteIntent(config: OcxConfig, version: string, deviceId: string): HomeRouteIntentDocument {
- return projectHomeRouteIntent(config, version, deviceId, readHomeRouteIntentState(config));
-}
-
-/** Provisioning runs under Home authority, before the read-only Child starts. */
-export function seedRouteDeviceClients(config: OcxConfig, version: string, deviceId: string, selected?: readonly RouteClientId[], activeDeviceIds?: readonly string[]): void {
- withConfigMutationLockSync(() => {
- if (activeDeviceIds) reconcileHomeRouteDevices(activeDeviceIds);
- initializeHomeRouteIntentDefaults(config, version);
- // Absence means inherit, not an implicit device selection that masks Home changes.
- if (selected === undefined) return;
- updateRouteIntentState(state => {
- const overrides = (state.devices[deviceId] ??= {});
- const revisions = ((state.revisions ??= {})[deviceId] ??= {});
- for (const client of ROUTE_CLIENT_IDS) {
- if (client !== "codex" && client !== "claude" && !selected.includes(client)) continue;
- overrides[client] = { ...(overrides[client] ?? {}), enabled: selected.includes(client) };
- revisions[client] = (revisions[client] ?? 0) + 1;
- }
- return state;
- });
- });
-}
-
-/** Trust retirement also retires policy/revisions; Home and live peers retain their state. */
-export function reconcileHomeRouteDevices(activeDeviceIds: readonly string[]): void {
- const live = new Set(["home", ...activeDeviceIds]);
- updateRouteIntentState(state => {
- for (const map of [state.devices, state.revisions]) {
- for (const id of Object.keys(map ?? {})) if (!live.has(id)) delete map![id];
- }
- return state;
- });
-}
diff --git a/src/routing/route-intent-state.ts b/src/routing/route-intent-state.ts
deleted file mode 100644
index cf209864fec..00000000000
--- a/src/routing/route-intent-state.ts
+++ /dev/null
@@ -1,178 +0,0 @@
-/** Home-owned desired policy. Reads never create or repair a file. */
-import { chmodSync, lstatSync, mkdirSync, readFileSync } from "node:fs";
-import { dirname, join } from "node:path";
-import { atomicWriteFile, isMissingPathError } from "../config/atomic-write";
-import { withConfigMutationLockSync } from "../config/mutation-lock";
-import { getConfigDir } from "../config/paths";
-import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
-import { hardenSecretDir } from "../lib/windows-secret-acl";
-import { emptyRouteIntentState, HOME_NATIVE_ROUTE_CLIENTS, isHomeNativeRouteClient, isRouteClientId, parseRouteClientIntent,
- type HomeNativeRouteClientId, type RouteClientId, type RouteClientIntent, type RouteIntentState } from "../link/route-intent";
-export type { RouteIntentState } from "../link/route-intent";
-
-const MAX_STATE_BYTES = 1024 * 1024;
-export function routeIntentStatePath(): string { return join(getConfigDir(), "route-intent.json"); }
-
-/**
- * `route-intent.json` exists but cannot be read or parsed. Writers answer it with a typed refusal
- * that names the file, because only the operator can fix or remove it.
- */
-export class RouteIntentStateUnreadableError extends Error {
- readonly path: string;
- constructor(path: string, cause: unknown) {
- super(`Route intent in ${path} could not be read; nothing was changed. Inspect it, fix or remove it, then retry.`, { cause });
- this.name = "RouteIntentStateUnreadableError";
- this.path = path;
- }
-}
-function deviceIdAllowed(value: string): boolean {
- return /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,255}$/.test(value) && !["__proto__", "constructor", "prototype"].includes(value);
-}
-function parseIntentMap(value: unknown): Partial> {
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid route intent map");
- const result: Partial> = {};
- for (const [client, raw] of Object.entries(value)) {
- if (!isRouteClientId(client)) throw new Error("unknown route client");
- const intent = parseRouteClientIntent(raw, client);
- if (!intent) throw new Error("invalid route client intent");
- result[client] = intent;
- }
- return result;
-}
-export function parseRouteIntentState(value: unknown): RouteIntentState {
- if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid route intent state");
- const raw = value as Record;
- if (raw.version !== 1 || Object.keys(raw).some(key => !["version", "defaults", "devices", "revisions"].includes(key)) || !Object.hasOwn(raw, "defaults") || !Object.hasOwn(raw, "devices")
- || !raw.devices || typeof raw.devices !== "object" || Array.isArray(raw.devices)) throw new Error("invalid route intent state");
- const state = emptyRouteIntentState();
- state.defaults = parseIntentMap(raw.defaults);
- for (const [id, map] of Object.entries(raw.devices)) {
- if (!deviceIdAllowed(id)) throw new Error("invalid route device id");
- state.devices[id] = parseIntentMap(map);
- }
- if (raw.revisions !== undefined) {
- if (!raw.revisions || typeof raw.revisions !== "object" || Array.isArray(raw.revisions)) throw new Error("invalid route device revisions");
- state.revisions = {};
- for (const [id, map] of Object.entries(raw.revisions)) {
- if (!deviceIdAllowed(id) || !map || typeof map !== "object" || Array.isArray(map)) throw new Error("invalid route device revisions");
- const revisions: Partial> = {};
- for (const [client, revision] of Object.entries(map)) {
- if (!isRouteClientId(client) || typeof revision !== "number" || !Number.isSafeInteger(revision) || revision < 0) throw new Error("invalid route client revision");
- revisions[client] = revision;
- }
- state.revisions[id] = revisions;
- }
- }
- return state;
-}
-export function readRouteIntentState(path = routeIntentStatePath()): RouteIntentState {
- try {
- const stat = lstatSync(path);
- if (!stat.isFile() || stat.size > MAX_STATE_BYTES) throw new Error("invalid route intent state file");
- return parseRouteIntentState(JSON.parse(readFileSync(path, "utf8")));
- } catch (error) {
- if (isMissingPathError(error)) return emptyRouteIntentState();
- throw new RouteIntentStateUnreadableError(path, error);
- }
-}
-function writeState(state: RouteIntentState, path: string): void {
- const dir = dirname(path);
- assertNotRealHomeUnderTest(dir);
- const body = `${JSON.stringify(state, null, 2)}\n`;
- if (Buffer.byteLength(body) > MAX_STATE_BYTES) throw new Error("route intent state too large");
- mkdirSync(dir, { recursive: true, mode: 0o700 });
- if (process.platform === "win32") hardenSecretDir(dir, { required: true });
- else chmodSync(dir, 0o700);
- atomicWriteFile(path, body);
-}
-export function writeRouteIntentState(state: RouteIntentState, path = routeIntentStatePath()): void {
- const parsed = parseRouteIntentState(state);
- withConfigMutationLockSync(() => writeState(parsed, path), dirname(path));
-}
-export function updateRouteIntentState(update: (state: RouteIntentState) => RouteIntentState, path = routeIntentStatePath()): RouteIntentState {
- return withConfigMutationLockSync(() => {
- const next = parseRouteIntentState(update(readRouteIntentState(path)));
- writeState(next, path);
- return next;
- }, dirname(path));
-}
-export function setRouteClientIntent(client: RouteClientId, intent: RouteClientIntent, deviceId?: string): RouteIntentState {
- const parsed = parseRouteClientIntent(intent, client);
- if (!isRouteClientId(client) || !parsed || (deviceId !== undefined && !deviceIdAllowed(deviceId))) throw new Error("invalid route intent selection");
- if (deviceId === "home" && isHomeNativeRouteClient(client)) throw new Error("A Home native decision is its native switch; use recordHomeNativeDecision");
- return updateRouteIntentState(state => {
- const map = deviceId === undefined ? state.defaults : (state.devices[deviceId] ??= {});
- map[client] = { ...map[client], ...parsed };
- if (deviceId !== undefined) advanceRevision(state, deviceId, client);
- return state;
- });
-}
-export function clearRouteClientIntent(client: RouteClientId, deviceId: string): RouteIntentState {
- if (!isRouteClientId(client) || !deviceIdAllowed(deviceId)) throw new Error("invalid route intent selection");
- if (deviceId === "home" && isHomeNativeRouteClient(client)) throw new Error("A Home native decision is its native switch; use recordHomeNativeDecision");
- return updateRouteIntentState(state => {
- const map = state.devices[deviceId];
- if (map) { delete map[client]; if (Object.keys(map).length === 0) delete state.devices[deviceId]; }
- advanceRevision(state, deviceId, client);
- return state;
- });
-}
-
-/**
- * Earlier versions recorded Home native decisions as `devices.home` entries. The Home's native
- * switches govern those clients now, so such an entry is dropped instead of pinning anything.
- */
-export function dropHomeNativeOverrides(state: RouteIntentState, clients: readonly HomeNativeRouteClientId[] = HOME_NATIVE_ROUTE_CLIENTS): void {
- const map = state.devices.home;
- if (!map) return;
- for (const client of clients) delete map[client];
- if (Object.keys(map).length === 0) delete state.devices.home;
-}
-
-/** Drops those legacy entries from the file, writing only when one is there. */
-export function dropLegacyHomeNativeOverrides(): void {
- withConfigMutationLockSync(() => {
- const map = readRouteIntentState().devices.home;
- if (!map || !HOME_NATIVE_ROUTE_CLIENTS.some(client => map[client] !== undefined)) return;
- updateRouteIntentState(state => { dropHomeNativeOverrides(state); return state; });
- });
-}
-
-/**
- * Bookkeeping for a Home decision about a native client, which itself is written to the Home's
- * native switch. No `devices.home` entry is kept. The caller has already captured the Children's
- * native defaults (src/routing/home-route-capture.ts), so the switch never becomes one.
- */
-export function recordHomeNativeDecision(client: HomeNativeRouteClientId): RouteIntentState {
- return updateRouteIntentState(state => {
- dropHomeNativeOverrides(state, [client]);
- return state;
- });
-}
-
-/** ocx sync applies route intent to every Home client once the file holds a default or a Home entry. */
-export function homeRouteIntentInUse(state: RouteIntentState): boolean {
- return Object.keys(state.defaults).length > 0 || Object.keys(state.devices.home ?? {}).length > 0;
-}
-
-/**
- * A file integration toggle on the Home (the dashboard, `PUT /api/client-integrations/`) is the
- * Home's own decision for that client. Once the Home uses route intent, ocx sync applies it to every
- * client, so the toggle is recorded as a `devices.home` entry and Children keep their defaults.
- * Without route intent nothing is written. Returns whether a decision was recorded.
- */
-export function recordHomeFileIntegrationDecision(client: RouteClientId, intent: RouteClientIntent): boolean {
- if (isHomeNativeRouteClient(client) || client === "aside") return false;
- return withConfigMutationLockSync(() => {
- if (!homeRouteIntentInUse(readRouteIntentState())) return false;
- setRouteClientIntent(client, intent, "home");
- return true;
- });
-}
-
-function advanceRevision(state: RouteIntentState, deviceId: string, client: RouteClientId): void {
- const revisions = ((state.revisions ??= {})[deviceId] ??= {});
- const next = (revisions[client] ?? 0) + 1;
- if (!Number.isSafeInteger(next)) throw new Error("route device revision exhausted");
- revisions[client] = next;
-}
diff --git a/src/routing/route-to-live-settings.ts b/src/routing/route-to-live-settings.ts
deleted file mode 100644
index a67095346d3..00000000000
--- a/src/routing/route-to-live-settings.ts
+++ /dev/null
@@ -1,35 +0,0 @@
-import { readConfigDiagnostics, withConfigMutationLockSync } from "../config";
-import type { OcxConfig } from "../types";
-
-/** Reflect controller-owned fields from current durable state, preserving concurrent settings. */
-export function mirrorRouteNativeSettings(live: OcxConfig, current: OcxConfig): void {
- for (const client of ["codex", "grok", "claude-desktop"] as const) {
- const value = current.clientIntegrations?.[client];
- if (value !== undefined) (live.clientIntegrations ??= {})[client] = value;
- else if (live.clientIntegrations) delete live.clientIntegrations[client];
- }
- for (const key of ["enabled", "authModeMigratedAt"] as const) {
- const value = current.claudeCode?.[key];
- if (value !== undefined) live.claudeCode = { ...live.claudeCode, [key]: value };
- else if (live.claudeCode) delete live.claudeCode[key];
- }
- const messages = current.apiSurfaces?.messages?.enabled;
- if (messages !== undefined) live.apiSurfaces = { ...live.apiSurfaces, messages: { ...live.apiSurfaces?.messages, enabled: messages } };
- else if (live.apiSurfaces?.messages) delete live.apiSurfaces.messages.enabled;
-}
-
-export function refreshRouteNativeSettings(live: OcxConfig): void {
- withConfigMutationLockSync(() => {
- const latest = readConfigDiagnostics();
- if (latest.source !== "file" || latest.error) throw new Error("config_unavailable");
- mirrorRouteNativeSettings(live, latest.config);
- });
-}
-
-/** A copy carrying the durable native switches; another process may have changed them. */
-export function withDurableRouteNativeSettings(config: OcxConfig): OcxConfig {
- const copy = structuredClone(config);
- const latest = readConfigDiagnostics();
- if (latest.source === "file" && !latest.error) mirrorRouteNativeSettings(copy, latest.config);
- return copy;
-}
diff --git a/src/routing/route-to-local.ts b/src/routing/route-to-local.ts
deleted file mode 100644
index 2b12d60d17d..00000000000
--- a/src/routing/route-to-local.ts
+++ /dev/null
@@ -1,88 +0,0 @@
-/** Local CLI reads/applies never relay management writes from a Child to its Home. */
-import { readClientConnectionState } from "../client/state";
-import { withLinkApply } from "../client/link-home";
-import { projectHomeRouteIntent, ROUTE_CLIENT_IDS, type RouteClientId, type RouteClientIntent, type RouteClientOutcome } from "../link/route-intent";
-import { initializeHomeRouteIntentDefaults, readHomeRouteIntentState } from "./home-route-intent";
-import { routeToDeviceStatus, type RouteToDeviceStatus, type RouteToStatus } from "./route-to-status";
-import { withDurableRouteNativeSettings } from "./route-to-live-settings";
-import { dropLegacyHomeNativeOverrides } from "./route-intent-state";
-import { readLocalRouteIntentOff, readRouteIntentReceipt, setLocalRouteIntentOff } from "../integrations/route-intent";
-import type { LinkChildContext } from "../cli/link-child";
-import type { OcxConfig } from "../types";
-
-export function readHomeRouteToStatus(config: OcxConfig, version: string): RouteToDeviceStatus {
- const local = withDurableRouteNativeSettings(config);
- const document = projectHomeRouteIntent(local, version, "home", readHomeRouteIntentState(local));
- const receipt = readRouteIntentReceipt();
- return routeToDeviceStatus("home", "home", document, receipt?.outcomes ?? [], receipt?.appliedRouteIntentDigest ?? null);
-}
-
-/**
- * `onlyClient` is the client a single Home write decides: only that client is materialized on the
- * Home. For a native client the decision (`onlyIntent`, else the default Children inherit) is
- * written through the Home's native switch. A sync passes none and never writes a Home native
- * switch: each native client is refreshed to match the switch as it is.
- */
-export async function syncHomeRouteTo(config: OcxConfig, version: string, overwriteConflict = false, overwriteClient?: RouteClientId, commit?: () => void,
- deps: { loadModels?: typeof import("../server/management/model-rows").loadExportModels; materialize?: typeof import("../client/route-intent-apply").materializeLocalRouteIntent;
- onlyClient?: RouteClientId; onlyIntent?: () => RouteClientIntent | undefined } = {}): Promise {
- return withLinkApply(async () => {
- commit?.();
- const local = withDurableRouteNativeSettings(config);
- // The Home's own apply is Home-only: it captures the Children's native defaults only once the
- // Home has a link. A default write's commit above has already captured them. Without a link it
- // saves no defaults, and only drops a legacy devices.home native entry if one is there.
- initializeHomeRouteIntentDefaults(local, version, undefined, { whenLinked: true });
- dropLegacyHomeNativeOverrides();
- const decidedIntent = deps.onlyIntent?.();
- const document = projectHomeRouteIntent(local, version, "home", readHomeRouteIntentState(local),
- { decidedClient: deps.onlyClient, ...(decidedIntent ? { decidedIntent } : {}) });
- const { loadExportModels } = await import("../server/management/model-rows");
- const { materializeLocalRouteIntent } = await import("../client/route-intent-apply");
- const materialize = deps.materialize ?? materializeLocalRouteIntent;
- const only = deps.onlyClient === undefined ? undefined : [deps.onlyClient];
- // A single-client write keeps the other clients' last proven outcomes in the Home receipt.
- const scope = only ? { retainSkippedOutcomes: true, decidedClient: deps.onlyClient } : {};
- const off = ROUTE_CLIENT_IDS.filter(client => !document.clients[client].enabled && (!only || only.includes(client)));
- await materialize(local, document, [], { overwriteConflict, overwriteClient, onlyClients: off, ...scope });
- const receipt = await materialize(local, document, await (deps.loadModels ?? loadExportModels)(local), { overwriteConflict, overwriteClient,
- ...(only ? { onlyClients: only } : {}), ...scope });
- return receipt.outcomes;
- });
-}
-
-function assertChild(context: LinkChildContext): void {
- const state = readClientConnectionState();
- if (state.kind !== "connected" || state.value.transport !== "link" || state.value.link?.linkId !== context.linkId) {
- throw new Error("The Remote Link connection changed; retry against the current device");
- }
-}
-
-export function readLocalRouteToStatus(context: LinkChildContext): RouteToStatus {
- assertChild(context);
- const receipt = readRouteIntentReceipt();
- if (!receipt) throw new Error("This Child has no recorded route intent. Run ocx route to sync to read and apply its Home intent");
- const stale = receipt.outcomes.filter(row => row.outcome === "stale").map(row => row.client);
- return { schema: "ocx.route-to/1", devices: [routeToDeviceStatus(context.linkId, "this", receipt.document,
- receipt.outcomes, receipt.appliedRouteIntentDigest, readLocalRouteIntentOff())],
- ...(receipt.catalogPending || stale.length ? { deferred: { reason: "intent_or_catalog_pending" as const, clients: stale } } : {}) };
-}
-
-export async function syncLocalRouteTo(context: LinkChildContext, overwriteConflict = false,
- deps: { sync?: typeof import("../client/connect").syncConnectedClient } = {}): Promise {
- assertChild(context);
- const { syncConnectedClient } = await import("../client/connect");
- const result = await (deps.sync ?? syncConnectedClient)({ expect: { kind: "link", linkId: context.linkId }, overwriteConflict });
- const status = readLocalRouteToStatus(context);
- if (result.stale) status.deferred ??= { reason: "intent_or_catalog_pending", clients: status.devices[0]!.clients.filter(row => row.applied === "stale").map(row => row.client) };
- return status;
-}
-
-export async function setLocalRouteToOff(context: LinkChildContext, client: RouteClientId, off: boolean): Promise {
- await withLinkApply(async () => {
- assertChild(context);
- setLocalRouteIntentOff(client, off);
- });
- // The existing sync takes the lifecycle/config locks and checks the pinned connection again.
- return syncLocalRouteTo(context);
-}
diff --git a/src/routing/route-to-status.ts b/src/routing/route-to-status.ts
deleted file mode 100644
index c944491ca62..00000000000
--- a/src/routing/route-to-status.ts
+++ /dev/null
@@ -1,81 +0,0 @@
-import { ROUTE_CLIENT_IDS, routeClientIntentDigest, routeIntentDigest, type HomeRouteIntentDocument, type RouteClientId, type RouteClientOutcome } from "../link/route-intent";
-
-export interface RouteToClientStatus {
- client: RouteClientId;
- intent: "on" | "off";
- /** `native`: a Home row governed by the Home's own persisted native switch. */
- source: "builtin" | "default" | "device" | "native" | "local";
- applied: RouteClientOutcome["outcome"] | null;
- target?: string;
-}
-
-export interface RouteToDeviceStatus {
- id: string;
- alias: string;
- desiredDigest: string;
- appliedRouteIntentDigest: string | null;
- clients: RouteToClientStatus[];
- /**
- * Present only on a device that never reads route intent: an ephemeral `ocx-child` refreshes the
- * Home catalog through ocx sync and nothing else, so it has no client rows.
- */
- routeIntent?: "unsupported";
- reason?: typeof EPHEMERAL_NO_ROUTE_INTENT;
-}
-
-/** Why an ephemeral `ocx-child` link has no route intent. */
-export const EPHEMERAL_NO_ROUTE_INTENT = "ephemeral-child-no-route-intent" as const;
-
-export type RouteToSyncRow = { id: string; state: "synced" | "failed" | "pending" | "unsupported"; reason?: string };
-
-export interface RouteToStatus {
- schema: "ocx.route-to/1";
- devices: RouteToDeviceStatus[];
- syncs?: RouteToSyncRow[];
- deferred?: { reason: "intent_or_catalog_pending"; clients: RouteClientId[] };
-}
-
-/** The status row of an ephemeral Child: route intent is unsupported, never stale or pending. */
-export function unsupportedRouteToDevice(id: string, alias: string): RouteToDeviceStatus {
- return { id, alias, desiredDigest: "", appliedRouteIntentDigest: null, clients: [], routeIntent: "unsupported", reason: EPHEMERAL_NO_ROUTE_INTENT };
-}
-
-/**
- * Compare receipts with today's intent: an outcome recorded for an earlier intent is stale, never
- * current. That includes an earlier conflict or failure, which says nothing about the new intent.
- */
-export function routeToDeviceStatus(
- id: string,
- alias: string,
- document: HomeRouteIntentDocument,
- outcomes: readonly RouteClientOutcome[],
- appliedDigest: string | null,
- localOff: readonly RouteClientId[] = [],
-): RouteToDeviceStatus {
- const effective = structuredClone(document);
- for (const row of outcomes) {
- if (row.initialIntent && row.initialRevision === (document.clientRevisions?.[row.client] ?? 0)) {
- effective.clients[row.client] = { ...effective.clients[row.client], ...row.initialIntent };
- effective.sources[row.client] = "device";
- }
- }
- const desiredDigest = routeIntentDigest(effective);
- return {
- id, alias, desiredDigest, appliedRouteIntentDigest: appliedDigest,
- clients: ROUTE_CLIENT_IDS.map(client => {
- const outcome = outcomes.find(row => row.client === client);
- const disabledLocally = localOff.includes(client) || outcome?.source === "local";
- // A receipt without per-client digests only proves its whole-document digest for current rows.
- const outdated = outcome !== undefined && !disabledLocally && outcome.outcome !== "stale"
- && (outcome.intentDigest ? outcome.intentDigest !== routeClientIntentDigest(effective, client)
- : outcome.outcome === "current" && desiredDigest !== appliedDigest);
- return {
- client,
- intent: disabledLocally ? "off" : effective.clients[client].enabled ? "on" : "off",
- source: disabledLocally ? "local" : effective.sources[client],
- applied: outdated ? "stale" : outcome?.outcome ?? null,
- ...(outcome?.target ? { target: outcome.target } : {}),
- };
- }),
- };
-}
diff --git a/src/routing/route-to-sync-child.ts b/src/routing/route-to-sync-child.ts
deleted file mode 100644
index e7c5e2d863a..00000000000
--- a/src/routing/route-to-sync-child.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { buildExecArgv, remotePinnedSyncArgv } from "../link/ssh-argv";
-import { classifySyncResult } from "../cli/sync-fanout";
-import { requireRemotePlatformFramed } from "../link/remote-platform";
-import { knownHostsFile, runnerFor } from "../server/management/link-route-state";
-import type { ManagementContext } from "../server/management/context";
-import type { RouteClientId } from "../link/route-intent";
-import type { LinkRecord } from "../link/store";
-import { homeRouteIntentWatch } from "../link/route-intent-watch";
-import { EPHEMERAL_NO_ROUTE_INTENT, type RouteToSyncRow } from "./route-to-status";
-
-/** Reuse pinned sync: never run on a machine that has disconnected or joined another Home. */
-export async function syncRouteToChild(link: LinkRecord, ctx: ManagementContext, overwriteConflict = false, overwriteClient?: RouteClientId): Promise> {
- // An ephemeral ocx-child never reads route intent; its catalog sync would apply none of it.
- if (link.childKind === "ephemeral") return { state: "unsupported", reason: EPHEMERAL_NO_ROUTE_INTENT };
- if (link.direction === "client-initiated") {
- const notified = homeRouteIntentWatch.notify(link.id);
- return { state: "pending", reason: overwriteConflict
- ? "Reconciliation requested without conflict overwrite; run ocx route to sync --overwrite-conflict on the Child to authorize local overwrites"
- : notified ? "Immediate reconciliation requested; awaiting the Child apply report"
- : "No active Child watcher; pending next sync tick. Run ocx route to sync on the Child for immediate apply" };
- }
- try {
- const runner = runnerFor(ctx);
- const knownHosts = knownHostsFile(ctx);
- const platform = link.remotePlatform ?? await requireRemotePlatformFramed(runner, link.alias, knownHosts);
- const result = await runner.run(buildExecArgv({ alias: link.alias, knownHostsFile: knownHosts, argv: remotePinnedSyncArgv(link.id, platform, overwriteConflict, overwriteClient) }), { timeoutMs: 60_000, maxOutputBytes: 64 * 1024 });
- const verdict = classifySyncResult(result);
- return verdict.state === "synced" ? { state: "synced" } : { state: "failed", reason: verdict.reason ?? "child-sync-failed" };
- } catch {
- return { state: "failed", reason: "child-sync-unavailable" };
- }
-}
diff --git a/src/server/auth-cors.ts b/src/server/auth-cors.ts
index 40ba18f5c16..da6e6e32f31 100644
--- a/src/server/auth-cors.ts
+++ b/src/server/auth-cors.ts
@@ -29,8 +29,6 @@ import {
providerHeadersConfigError,
providerForwardClientHeadersConfigError,
reasoningSummaryDeliveryRecordConfigError,
- thinkingDisplayConfigError,
- modelThinkingDisplayConfigError,
upstreamHttpVersionConfigError,
credentialPinnedProviderConfigError,
} from "../config/provider-validation";
@@ -951,10 +949,6 @@ export function providerManagementConfigError(
raw.modelSupportsReasoningSummaries,
);
if (reasoningSummaryDeliveryError) return `provider ${name} ${reasoningSummaryDeliveryError}`;
- const thinkingDisplayError = thinkingDisplayConfigError(raw.thinkingDisplay);
- if (thinkingDisplayError) return `provider ${name} ${thinkingDisplayError}`;
- const modelThinkingDisplayError = modelThinkingDisplayConfigError(raw.modelThinkingDisplay);
- if (modelThinkingDisplayError) return `provider ${name} ${modelThinkingDisplayError}`;
const modelAdaptersError = modelAdapterRecordConfigError(raw.modelAdapters, "modelAdapters", name, typed);
if (modelAdaptersError) return `provider ${name} ${modelAdaptersError}`;
const credentialPinError = credentialPinnedProviderConfigError(name, typed);
@@ -1181,8 +1175,6 @@ const PROVIDER_CONFIG_FIELD_POLICY = {
modelSupportsVerbosity: "editor",
supportsVerbosity: "editor",
modelReasoningSummaryDelivery: "editor",
- thinkingDisplay: "editor",
- modelThinkingDisplay: "editor",
modelPreferHostedTools: "editor",
supportsOpenAiWebSearchToolFields: "editor",
xaiResponsesXSearch: "editor",
diff --git a/src/server/index/link-listener.ts b/src/server/index/link-listener.ts
index bc1e088314c..4f020381c9c 100644
--- a/src/server/index/link-listener.ts
+++ b/src/server/index/link-listener.ts
@@ -11,7 +11,6 @@ import { linkStorePath } from "../../link/paths";
import { linkRouteAllowed } from "../../link/routes";
import { withConfigMutationLockSync } from "../../config/mutation-lock";
import { registerFallbackLinkResolver } from "../../routing/fallback-policy";
-import { captureChildRouteDefaults } from "../../routing/home-route-intent";
export const LINK_INGRESS_HOSTNAME = "opencodex-link.invalid";
@@ -37,10 +36,6 @@ export interface LinkListenerDeps {
writeStore?: (path: string, store: LinkStore) => void;
serve?: (options: Parameters[0]) => Server;
warn?: (message: string) => void;
- /** Freezes every default Children inherit (native switches and file ownership) before this listener can serve route intent. */
- captureRouteDefaults?: () => void;
- /** Delay before retry `attempt` (0-based) of a failed capture; tests shorten it. */
- captureRetryDelayMs?: (attempt: number) => number;
}
export type LinkListenerStatus = {
@@ -105,32 +100,6 @@ export function createLinkListenerLifecycle(deps: LinkListenerDeps = {}): Lin
const releaseFallbackLinkResolver = registerFallbackLinkResolver(keyId =>
readStoreForAdmission().links.find(link => link.apiKeyId === keyId)?.id);
- // Children read route intent through this listener, and that read is pure: it never captures.
- // So a failed capture (a busy config lock, a damaged route-intent.json, unresolved Aside
- // ownership) is owed and retried with backoff until it succeeds, the listener closes or stops,
- // and again on every ensureStarted. Meanwhile every Home-only writer still captures first.
- let routeDefaultsOwed = false;
- let captureAttempt = 0;
- let captureRetry: ReturnType | undefined;
- const retryDelay = deps.captureRetryDelayMs ?? (attempt => Math.min(1_000 * 2 ** attempt, 300_000));
- const captureRouteDefaults = (): void => {
- if (captureRetry) { clearTimeout(captureRetry); captureRetry = undefined; }
- try {
- (deps.captureRouteDefaults ?? captureChildRouteDefaults)();
- routeDefaultsOwed = false;
- captureAttempt = 0;
- } catch (error) {
- if (!routeDefaultsOwed) warn(`⚠ Child route defaults were not captured; retrying: ${error instanceof Error ? error.message : String(error)}`);
- routeDefaultsOwed = true;
- if (stopped) return;
- captureRetry = setTimeout(() => {
- captureRetry = undefined;
- if (!stopped && routeDefaultsOwed) captureRouteDefaults();
- }, retryDelay(captureAttempt++));
- (captureRetry as { unref?: () => void }).unref?.();
- }
- };
-
const bindIfNeeded = (): void => {
if (stopped || listener || !startContext) return;
let store: LinkStore;
@@ -144,10 +113,6 @@ export function createLinkListenerLifecycle(deps: LinkListenerDeps = {}): Lin
setStatus("off", null, null);
return;
}
- // Capture the Children's defaults from the Home's switches and integrations as they stand at
- // startup, before any Home-only command can move one. A failure leaves the listener up and is
- // retried (see above).
- captureRouteDefaults();
const requestedPort = store.listenerPort ?? 0;
const sessions = createLinkRelaySessions({ peers, connectionGeneration: linkId => startContext?.connectionGeneration?.(linkId), fingerprints: (keyId, linkId) => {
if (!readStoreForAdmission().links.some(link => link.id === linkId && link.apiKeyId === keyId)) return [];
@@ -211,8 +176,6 @@ export function createLinkListenerLifecycle(deps: LinkListenerDeps = {}): Lin
const flight = (async () => {
if (closeFlight) await closeFlight;
if (stopped) return;
- // An unbound listener captures in bindIfNeeded; a bound one retries an owed capture here.
- if (routeDefaultsOwed && listener) captureRouteDefaults();
bindIfNeeded();
})();
ensureFlight = flight.finally(() => {
@@ -224,9 +187,6 @@ export function createLinkListenerLifecycle(deps: LinkListenerDeps = {}): Lin
const close = (): Promise => {
if (closeFlight) return closeFlight;
- // No link is left to protect: an owed capture is retried at the next bind, not on a timer.
- if (captureRetry) { clearTimeout(captureRetry); captureRetry = undefined; }
- captureAttempt = 0;
const flight = (async () => {
if (ensureFlight) await ensureFlight;
const current = listener;
@@ -271,7 +231,6 @@ export function createLinkListenerLifecycle(deps: LinkListenerDeps = {}): Lin
close,
async stop() {
stopped = true;
- if (captureRetry) { clearTimeout(captureRetry); captureRetry = undefined; }
releaseFallbackLinkResolver();
// Process shutdown is deliberately forceful, unlike last-link removal. Every
// authenticated socket is closed, and the client transport cannot reconnect it.
diff --git a/src/server/index/link-management.ts b/src/server/index/link-management.ts
index bb264d8c5c1..a6b8e3a9c99 100644
--- a/src/server/index/link-management.ts
+++ b/src/server/index/link-management.ts
@@ -12,9 +12,7 @@ import type { OcxConfig } from "../../types";
import { handleManagementAPI, type ManagementApiDeps } from "../management-api";
import type { ManagementSessionControl } from "../management-auth";
import { runPassiveRead } from "../../lib/passive-read";
-import { LINK_CODEX_SETTINGS_PATH, LINK_ROUTE_INTENT_PATH, LINK_ROUTE_CATALOG_PATH, linkManagementRouteAllowed, linkManagementTarget } from "../../link/management-routes";
-import { projectRouteCatalogFacts } from "../../integrations/route-intent-catalog";
-import { readLinkRouteIntent } from "./route-intent-watch";
+import { LINK_CODEX_SETTINGS_PATH, linkManagementRouteAllowed, linkManagementTarget } from "../../link/management-routes";
import { projectHomeCodexSettings } from "../../link/codex-settings";
import { homeLinkChildParity, LINK_CHILD_PARITY_MAX_BYTES, LINK_CHILD_PARITY_PATH, parseLinkChildParityReport } from "../../link/child-parity";
import { linkStorePath } from "../../link/paths";
@@ -78,21 +76,9 @@ export async function handleLinkManagementRequest(
deps: ManagementApiDeps,
sessionControl?: ManagementSessionControl,
admittedKeyId?: string,
- /** Repeats link-key admission for a request that waited; see readLinkRouteIntent. */
- readmit?: () => string | undefined,
): Promise {
if (url.pathname === LINK_CHILD_PARITY_PATH) return handleChildParityReport(req, url, admittedKeyId);
if (!linkManagementRouteAllowed(url, req)) return notFound();
- if (url.pathname === LINK_ROUTE_CATALOG_PATH) {
- if (!admittedKeyId) return refused(403, "link_key_required");
- try {
- if (readLinkStore(linkStorePath()).links.filter(link => link.apiKeyId === admittedKeyId).length !== 1) return refused(403, "link_mismatch");
- return Response.json(projectRouteCatalogFacts(config), { headers: { "cache-control": "no-store" } });
- } catch { return refused(503, "route_catalog_unavailable"); }
- }
- if (url.pathname === LINK_ROUTE_INTENT_PATH) {
- return readLinkRouteIntent(req, config, VERSION, admittedKeyId, readmit);
- }
if (url.pathname === LINK_CODEX_SETTINGS_PATH) {
return Response.json(projectHomeCodexSettings(config, VERSION), { headers: { "cache-control": "no-store" } });
}
diff --git a/src/server/index/route-intent-watch.ts b/src/server/index/route-intent-watch.ts
deleted file mode 100644
index f68f9f12fed..00000000000
--- a/src/server/index/route-intent-watch.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import type { OcxConfig } from "../../types";
-import { routeIntentDigest, ROUTE_INTENT_DIGEST_PATTERN } from "../../link/route-intent";
-import { homeRouteIntentWatch, ROUTE_INTENT_AFTER_HEADER, ROUTE_INTENT_WAKE_HEADER, ROUTE_INTENT_WATCH_HEADER } from "../../link/route-intent-watch";
-import { projectChildRouteIntent } from "../../routing/home-route-intent";
-import { linkStorePath } from "../../link/paths";
-import { readLinkStore } from "../../link/store";
-
-function refused(status: number, error: string): Response {
- return Response.json({ error }, { status, headers: { "cache-control": "no-store" } });
-}
-/**
- * Called only after the shared exact GET/no-query/no-upgrade gate. `readmit` repeats the
- * listener's key admission against current policy and returns the key id it admits now.
- */
-export async function readLinkRouteIntent(req: Request, config: OcxConfig, version: string, admittedKeyId?: string,
- readmit?: () => string | undefined): Promise {
- if (!admittedKeyId) return refused(403, "link_key_required");
- const watch = req.headers.get(ROUTE_INTENT_WATCH_HEADER);
- const after = req.headers.get(ROUTE_INTENT_AFTER_HEADER);
- if ((watch !== null && watch !== "1") || (after !== null && (watch !== "1" || !ROUTE_INTENT_DIGEST_PATTERN.test(after)))) {
- return refused(400, "invalid_route_intent_watch");
- }
- try {
- const ownedLink = () => {
- const links = readLinkStore(linkStorePath()).links.filter(link => link.apiKeyId === admittedKeyId);
- return links.length === 1 ? links[0]!.id : null;
- };
- const linkId = ownedLink();
- if (!linkId) return refused(403, "link_mismatch");
- const project = () => projectChildRouteIntent(config, version, linkId);
- let document = project();
- let wakeReason: "changed" | "notified" | "timeout" = "changed";
- if (watch === "1" && after === routeIntentDigest(document)) {
- const wake = await homeRouteIntentWatch.wait(linkId, req.signal);
- if (wake === "cancelled") return refused(499, "route_intent_watch_cancelled");
- if (wake === "busy") return refused(429, "route_intent_watch_busy");
- wakeReason = wake;
- // Trust, the key itself and effective policy can all have changed during the wait.
- if (readmit && readmit() !== admittedKeyId) return refused(403, "link_key_required");
- if (ownedLink() !== linkId) return refused(403, "link_mismatch");
- document = project();
- }
- if (req.signal.aborted) return refused(499, "route_intent_watch_cancelled");
- return Response.json(document, { headers: { "cache-control": "no-store", [ROUTE_INTENT_WATCH_HEADER]: "1",
- ...(watch === "1" ? { [ROUTE_INTENT_WAKE_HEADER]: wakeReason } : {}) } });
- } catch { return refused(503, "route_intent_unavailable"); }
-}
diff --git a/src/server/index/serve-options.ts b/src/server/index/serve-options.ts
index 6abe81aaaa5..867228df12b 100644
--- a/src/server/index/serve-options.ts
+++ b/src/server/index/serve-options.ts
@@ -398,7 +398,6 @@ export function createServeOptions(ctx: ServeOptionsContext) {
return withCors(await handleLinkManagementRequest(
req, requestUrl, config, requestManagementApiDeps, managementSessionControl,
admission.kind === "configured" ? admission.keyId : undefined,
- () => { const current = resolveApiAuth(req, linkPolicy()); return current?.kind === "configured" ? current.keyId : undefined; },
), req, policy);
}
}
diff --git a/src/server/management-api.ts b/src/server/management-api.ts
index d8f4f7df9ca..dc206ce4e2e 100644
--- a/src/server/management-api.ts
+++ b/src/server/management-api.ts
@@ -176,12 +176,6 @@ async function handleFallbackRoutesOnDemand(ctx: ManagementContext): Promise {
- if (!pathInManagementNamespace(ctx.url.pathname, "/api/route-to")) return null;
- const { handleRouteToRoutes } = await import("./management/route-to-routes");
- return handleRouteToRoutes(ctx);
-}
-
/**
* Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
* mounted for every dashboard request, so a static import would put the workflow-budget ledger
@@ -376,7 +370,6 @@ export async function handleManagementAPI(
?? (await handleLowQuotaRoutesOnDemand(ctx))
?? (await handleQuotaPoolRoutesOnDemand(ctx))
?? (await handleFallbackRoutesOnDemand(ctx))
- ?? (await handleRouteToRoutesOnDemand(ctx))
?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
?? (await handleCodexAgentRoleRoutesOnDemand(ctx))
?? (await handleProtocolRoutesOnDemand(ctx))
diff --git a/src/server/management/agent-settings-routes.ts b/src/server/management/agent-settings-routes.ts
index 0e2dccb6648..a3f27971255 100644
--- a/src/server/management/agent-settings-routes.ts
+++ b/src/server/management/agent-settings-routes.ts
@@ -9,7 +9,6 @@ import { ensureManagementClaudeIntercept, interceptStartRefusal, interceptStatus
import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
import { captureDesktopAppliedMarker, commitDesktopAppliedMarker } from "../../claude/desktop-applied-marker";
import { isPassiveRead } from "../../lib/passive-read";
-import { captureHomeRouteDefaultsBeforeHomeSwitch } from "../../routing/home-route-capture";
import { randomUUID } from "node:crypto";
import { readFileSync } from "node:fs";
import { dirname } from "node:path";
@@ -1122,9 +1121,9 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise {
- const result = setHomeIntegrationEnabled("claude-desktop", true);
+ const result = setIntegrationEnabled("claude-desktop", true);
if (result.ok) mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
return result;
});
@@ -1213,10 +1212,9 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise {
- const result = setHomeIntegrationEnabled("claude-desktop", true);
+ const result = setIntegrationEnabled("claude-desktop", true);
if (result.ok) mirrorDesiredEnabledOntoSnapshot(config, "claude-desktop", true);
return result;
});
@@ -1843,10 +1841,6 @@ async function handleAgentSettingsRoutesImpl(ctx: ManagementContext): Promise RouteClientIntent): Response {
- try {
- if (unlinkedUnreadableState()) return success;
- if (clientId === "aside") recordHomeAsideDecision(durableConfig(ctx.config));
- else if (isRouteClientId(clientId) && intent) recordHomeFileIntegrationDecision(clientId, intent());
- return success;
- } catch (error) {
- const reason = error instanceof HomeAsideIntentDerivationError
- ? "its Aside profiles could not be named by stable userIds"
- : `route intent in ${routeIntentStatePath()} could not record it`;
- return jsonResponse({ error: `The ${clientId} integration was changed, but ${reason}, so ocx sync may undo it. Inspect it, fix or remove it, then make the change again.`,
- code: "route_intent_unavailable", clientId, integrationChanged: true }, 500, ctx.req, ctx.config);
- }
-}
diff --git a/src/server/management/integration-routes.ts b/src/server/management/integration-routes.ts
index 2ca717145b7..1d95ac15456 100644
--- a/src/server/management/integration-routes.ts
+++ b/src/server/management/integration-routes.ts
@@ -48,8 +48,6 @@ import {
setIntegrationMutationFlightTestHook,
} from "../../integrations/mutation-flight";
import { jsonResponse } from "../auth-cors";
-import { isRouteClientId, type RouteClientIntent } from "../../link/route-intent";
-import { prepareHomeIntegrationDecision, recordHomeIntegrationDecision } from "./home-integration-decision";
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
import { managementInferencePort, type ManagementContext } from "./context";
import { exportSnapshotIdentity, loadExportModels, previewExportSnapshot } from "./model-rows";
@@ -283,33 +281,6 @@ function asideOptions(ctx: ManagementContext): AsideProfileRouteOptions {
};
}
-/**
- * Which Aside requests are a Home decision, and the profiles each may enable: a toggle of one
- * profile or all of them, or a profile restore. Undefined for reads, previews, journal and sync.
- */
-async function asideDecisionScope(ctx: ManagementContext, known: unknown): Promise {
- // The paths handleAsideProfileRoutes owns: the whole set, the profile collection, one profile,
- // and one profile's restore.
- const match = /^\/api\/client-integrations\/aside(?:\/profiles(?:\/(0|[1-9][0-9]{0,8})(\/restore)?)?)?$/.exec(ctx.url.pathname);
- if (!match) return undefined;
- const restore = match[2] !== undefined && ctx.req.method === "POST";
- const toggle = match[2] === undefined && ctx.req.method === "PUT";
- if (!toggle && !restore) return undefined;
- const raw = match[1] ?? ctx.url.searchParams.get("profile");
- const id = raw !== null && /^(0|[1-9][0-9]{0,8})$/.test(raw) ? Number(raw) : undefined;
- if (restore) return id === undefined ? [] : [id];
- const body = known !== undefined ? known : await readJsonBody({ ...ctx, req: ctx.req.clone() });
- if (!isPlainRecord(body) || body.enabled !== true) return [];
- return id === undefined ? "all" : [id];
-}
-
-/** A file client's Home decision as its ownership record stands after a restore. */
-function ownedIntent(store: IntegrationStateStore, clientId: IntegrationClientId): RouteClientIntent {
- const owner = store.readRecords()[clientId];
- return { enabled: Boolean(owner),
- ...(owner?.modelNames && (clientId === "hermes" || clientId === "commandcode") ? { modelNames: owner.modelNames } : {}) };
-}
-
async function buildIntegrationWriteInput(
clientId: IntegrationClientId,
ctx: ManagementContext,
@@ -715,12 +686,10 @@ async function handleJournalDelete(ctx: ManagementContext): Promise {
export async function handleIntegrationRoutes(ctx: ManagementContext): Promise {
// Aside delegates before the ordinary client handler. Reject unsupported intent before that
// delegation can enumerate profiles, load catalogs, or write a different preference.
- let asideBody: unknown;
if (/^\/api\/client-integrations\/aside\/profiles(?:\/|$)/.test(ctx.url.pathname)
&& (ctx.req.method === "PUT" || ctx.req.method === "POST")) {
const body = await readJsonBody({ ...ctx, req: ctx.req.clone() });
if (body instanceof Response) return body;
- asideBody = body;
if (isPlainRecord(body)) {
const naming = readModelNames(ctx, body, "aside", "apply");
if (naming instanceof Response) return naming;
@@ -739,14 +708,8 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise= 200 && aside.status < 300 ? recordHomeIntegrationDecision(ctx, "aside", aside) : aside;
+ if (aside) return aside;
if (url.searchParams.has("profile")
&& (url.pathname === INTEGRATION_COLLECTION_PATH || url.pathname.startsWith(INTEGRATION_ROUTE_PREFIX))
&& !INTEGRATION_HISTORY_PATHS.includes(url.pathname)) {
@@ -1003,20 +966,13 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise= 200 && asideRestore.status < 300
- ? recordHomeIntegrationDecision(ctx, "aside", asideRestore) : asideRestore;
+ if (asideRestore) return asideRestore;
let restoreClientId: IntegrationClientId | undefined;
try {
const store = integrationStore(ctx);
@@ -1082,9 +1038,7 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise ownedIntent(store, operation.clientId)) : restored;
+ return jsonResponse(result satisfies IntegrationRestoreEnvelope, 200, req, ctx.config);
} catch (error) {
if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
return jsonResponse({
@@ -1177,14 +1131,6 @@ export async function handleIntegrationRoutes(ctx: ManagementContext): Promise ({ enabled: parsed.enabled === true,
- ...(modelNames !== undefined && (requestedClient === "hermes" || requestedClient === "commandcode") ? { modelNames } : {}) })) : toggled;
+ return jsonResponse(result satisfies IntegrationToggleEnvelope, 200, req, ctx.config);
} catch (error) {
if (error instanceof IntegrationMutationBusyError || error instanceof IntegrationWriterLockBusyError) {
return jsonResponse({
diff --git a/src/server/management/link-child-preflight.ts b/src/server/management/link-child-preflight.ts
index d3332f03dee..6bcedc8ce90 100644
--- a/src/server/management/link-child-preflight.ts
+++ b/src/server/management/link-child-preflight.ts
@@ -1,4 +1,4 @@
-import { SSH_MAX_OUTPUT_BYTES, boundHint, sshFailureHint } from "../../link/ssh-runner";
+import { SSH_MAX_OUTPUT_BYTES } from "../../link/ssh-runner";
/**
* Home enrollment reads one Child command before it may restart that Child: `ocx config show
@@ -19,56 +19,3 @@ export const CHILD_CONFIG_TOO_LARGE_MESSAGE = "The Child configuration is too la
/** The operator-facing hint for a Child whose config output exceeded even the raised cap. */
export const CHILD_CONFIG_TOO_LARGE_HINT =
`ocx config show --json on the Child exceeded ${CHILD_PREFLIGHT_MAX_OUTPUT_BYTES} bytes; trim the Child's saved configuration and retry`;
-
-/**
- * Why the Child's effective configuration could not be read (#8357): a failed `config show`
- * names its own last stderr line (bounded and redacted by `sshFailureHint`, e.g. an invalid
- * saved configuration), and a successful one that printed no usable JSON says so.
- */
-export function childConfigFailureHint(result: { code: number; stderr: string }): string {
- if (result.code !== 0) return sshFailureHint(result.stderr) ?? `ocx config show --json exited ${result.code} on the Child`;
- return "ocx config show --json on the Child did not print a configuration with a valid port";
-}
-
-/** `ocx config validate --json` prints one small object; this bounds a misbehaving Child. */
-export const CHILD_CONFIG_VALIDATE_MAX_OUTPUT_BYTES = 16 * 1024;
-
-/**
- * The validation reason a full `ocx` Child gives for its own saved configuration, or undefined.
- *
- * `config show` on an invalid configuration prints only a generic warning whose path can push
- * the pointer past the 160-code-point hint cap, so it never names the defect (#8357). `config
- * validate --json` answers `{ "ok": false, "error": "schema_invalid: : " }`, naming
- * fields and expected types, never configured values. Only that `error` string is read from
- * stdout, and `boundHint` still redacts and caps it. A Child without the command (`ocx-child`,
- * an older `ocx`) prints nothing usable and the caller keeps the `config show` reason.
- */
-export function childConfigValidationHint(result: { code: number; stdout: string }): string | undefined {
- if (result.code === 0) return undefined;
- let parsed: unknown;
- try { parsed = JSON.parse(result.stdout); } catch { return undefined; }
- if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) return undefined;
- const { ok, error } = parsed as { ok?: unknown; error?: unknown };
- if (ok !== false || typeof error !== "string") return undefined;
- return boundHint(`Child config is invalid: ${error}`);
-}
-
-/**
- * The hint for an unreadable Child configuration: the Child's own validation reason when `config
- * show` failed and `config validate` can name it, otherwise `childConfigFailureHint`. The
- * extra command runs only on this failure path and is read-only on the Child.
- */
-export async function childConfigFailureReason(
- configured: { code: number; stderr: string },
- validate: () => Promise<{ code: number; stdout: string }>,
-): Promise {
- if (configured.code !== 0) {
- try {
- const reason = childConfigValidationHint(await validate());
- if (reason) return reason;
- } catch {
- // A timeout or transport failure here must not mask the config show reason.
- }
- }
- return childConfigFailureHint(configured);
-}
diff --git a/src/server/management/link-enrollment.ts b/src/server/management/link-enrollment.ts
index 0d58e8c3c2c..c157e964fa9 100644
--- a/src/server/management/link-enrollment.ts
+++ b/src/server/management/link-enrollment.ts
@@ -1,4 +1,3 @@
-import { seedRouteDeviceClients } from "../../routing/home-route-intent";
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
@@ -15,7 +14,7 @@ import { withConfigMutationLockSync } from "../../config/mutation-lock";
import type { ManagementContext } from "./context";
import { readManagementJsonBodyOr } from "./body";
import { fail, exactBody, port, isRecord, runnerFor, readStoreFor, writeStoreFor, knownHostsFile, putKnownHost, issueKey, compensateNewLink, type LinkRouteState } from "./link-route-state";
-import { CHILD_CONFIG_TOO_LARGE_HINT, CHILD_CONFIG_TOO_LARGE_MESSAGE, CHILD_CONFIG_VALIDATE_MAX_OUTPUT_BYTES, CHILD_PREFLIGHT_MAX_OUTPUT_BYTES, childConfigFailureReason } from "./link-child-preflight";
+import { CHILD_CONFIG_TOO_LARGE_HINT, CHILD_CONFIG_TOO_LARGE_MESSAGE, CHILD_PREFLIGHT_MAX_OUTPUT_BYTES } from "./link-child-preflight";
const PROBE_TTL_MS = 5 * 60_000;
const MIN_REMOTE_OCX = [2, 66, 0] as const;
@@ -290,10 +289,7 @@ export async function apply(ctx: ManagementContext, state: LinkRouteState): Prom
const configured = await run(ocx(["config", "show", "--json"]), budget, CHILD_PREFLIGHT_MAX_OUTPUT_BYTES);
let childConfig: unknown = null;
try { if (configured.code === 0) childConfig = JSON.parse(configured.stdout); } catch { /* fail closed */ }
- if (!isRecord(childConfig) || !port(childConfig.port)) {
- const hint = await childConfigFailureReason(configured, () => run(ocx(["config", "validate", "--json"]), budget, CHILD_CONFIG_VALIDATE_MAX_OUTPUT_BYTES));
- return await failed(fail("child_preflight_failed", "The Child effective configuration could not be read.", 502, hint));
- }
+ if (!isRecord(childConfig) || !port(childConfig.port)) return await failed(fail("child_preflight_failed", "The Child effective configuration could not be read.", 502));
if ((childConfig.runtimeRole ?? "standalone") !== "standalone") return await failed(fail("child_restart_unsupported", "The Child must run standalone OpenCodex; an existing Hub or Child cannot be converted by Add Child.", 409));
const childPort = childConfig.port;
if (remotePlatform === "posix" && (await run(REMOTE_CURL_PREFLIGHT)).code !== 0) return await failed(fail("child_curl_missing", "Install curl on the Child before connecting a Remote Link.", 409));
@@ -315,9 +311,6 @@ export async function apply(ctx: ManagementContext, state: LinkRouteState): Prom
if (current.links.some(link => link.alias === alias && link.direction === "hub-initiated")) throw new Error("link exists");
writeStoreFor(ctx, { ...current, links: [...current.links, record!] });
});
- // An ephemeral ocx-child never reads route intent, so its enrollment saves none: seeding would
- // also start ocx sync applying route intent to every Home client.
- if (!ephemeralChild) withConfigMutationLockSync(() => seedRouteDeviceClients(ctx.config, ctx.version, id, undefined, readStoreFor(ctx).links.map(link => link.id)));
await state.listener.ensureStarted();
if (state.listener.status().state !== "listening") return await failed(fail("listener_unavailable", "The link listener is unavailable.", 503));
await state.supervisor.ensureStarted();
diff --git a/src/server/management/link-route-state.ts b/src/server/management/link-route-state.ts
index 42dc39d8490..d58522417c4 100644
--- a/src/server/management/link-route-state.ts
+++ b/src/server/management/link-route-state.ts
@@ -6,7 +6,6 @@ import { clearCompensationFailed, compensationPath, markCompensationFailed } fro
import { readLinkStore, writeLinkStore, type LinkStore } from "../../link/store";
import { boundHint, createSshRunner, type SshRunner } from "../../link/ssh-runner";
import { withConfigMutationLockSync } from "../../config/mutation-lock";
-import { reconcileHomeRouteDevices } from "../../routing/home-route-intent";
import type { ManagementContext } from "./context";
import { issueApiKeyInProcess, revokeApiKeyInProcess, type IssuedApiKey } from "./oauth-account-routes";
@@ -182,7 +181,6 @@ export async function compensateNewLink(ctx: ManagementContext, state: LinkRoute
if (filtered.links.length !== current.links.length) {
writeStoreFor(ctx, filtered);
}
- reconcileHomeRouteDevices(filtered.links.map(link => link.id));
return filtered;
});
clearCompensationFailed(record.id, compensationPath());
diff --git a/src/server/management/link-routes.ts b/src/server/management/link-routes.ts
index fc90a0961d6..116a498c725 100644
--- a/src/server/management/link-routes.ts
+++ b/src/server/management/link-routes.ts
@@ -1,5 +1,3 @@
-import { isRouteClientId, ROUTE_CLIENT_IDS } from "../../link/route-intent";
-import { seedRouteDeviceClients, reconcileHomeRouteDevices } from "../../routing/home-route-intent";
import { probe, confirmHost, apply } from "./link-enrollment";
import { fail, pairedSession, auth, port, isRecord, exactBody, runnerFor, readStoreFor, writeStoreFor, knownHostsFile, issueKey, revokeKeyIdempotent, compensateNewLink, type LinkRouteState } from "./link-route-state";
export type { LinkRouteState, PendingLinkHost, ConfirmedLinkHost, LinkRouteListener } from "./link-route-state";
@@ -77,12 +75,9 @@ async function candidates(ctx: ManagementContext): Promise {
}
async function issue(ctx: ManagementContext): Promise {
- const raw = await readManagementJsonBodyOr(ctx.req, null);
- const body = exactBody(raw, isRecord(raw) && Object.hasOwn(raw, "selectedClients") ? ["alias", "tunnelPort", "selectedClients"] : ["alias", "tunnelPort"]);
+ const body = exactBody(await readManagementJsonBodyOr(ctx.req, null), ["alias", "tunnelPort"]);
if (!body || typeof body.alias !== "string" || !isLinkPort(body.tunnelPort)) return fail("invalid_body", "alias and tunnelPort are required.", 400);
try { assertSshAlias(body.alias); } catch { return fail("invalid_alias", "alias must be a valid SSH host alias.", 400); }
- const selection = body.selectedClients;
- if (selection !== undefined && (!Array.isArray(selection) || selection.length === 0 || selection.length > ROUTE_CLIENT_IDS.length || !selection.every(isRouteClientId) || new Set(selection).size !== selection.length)) return fail("invalid_body", "selectedClients must contain unique supported clients", 400);
let issued: IssuedApiKey;
try { issued = issueKey(ctx, `link:${body.alias}`); }
catch { return fail("key_issue_failed", "The link key could not be issued.", 503); }
@@ -110,7 +105,6 @@ async function issue(ctx: ManagementContext): Promise {
const compensation = await compensateNewLink(ctx, state, record);
return compensation ?? fail("link_exists", "A link for this alias already exists.", 409);
}
- withConfigMutationLockSync(() => seedRouteDeviceClients(ctx.config, ctx.version, id, selection as import("../../link/route-intent").RouteClientId[] | undefined, readStoreFor(ctx).links.map(link => link.id)));
await state.listener.ensureStarted();
if (state.listener.status().state !== "listening") {
failureCode = "listener_unavailable";
@@ -261,7 +255,6 @@ async function remove(ctx: ManagementContext, state: LinkRouteState, id: string)
const latest = readStoreFor(ctx);
const filtered = { ...latest, links: latest.links.filter(link => link.id !== id) };
if (filtered.links.length !== latest.links.length) writeStoreFor(ctx, filtered);
- reconcileHomeRouteDevices(filtered.links.map(link => link.id));
return filtered;
});
clearCompensationFailed(id, compensationPath());
diff --git a/src/server/management/native-integration-routes.ts b/src/server/management/native-integration-routes.ts
index 6dd26a34bde..be01108fbf8 100644
--- a/src/server/management/native-integration-routes.ts
+++ b/src/server/management/native-integration-routes.ts
@@ -6,12 +6,6 @@ import { mirrorDesiredEnabledOntoSnapshot } from "./claude-desktop-config-writer
import { ensureManagementClaudeIntercept, interceptStartRefusal } from "./claude-intercept-routes";
import { persistCommittedDesktopGateway } from "../../claude/desktop-gateway-state";
import { commitClaudeCodeBlock } from "../../claude/claude-code-block";
-import { HomeAsideIntentDerivationError, initializeHomeRouteIntentDefaults } from "../../routing/home-route-intent";
-import { homeHasLinks } from "../../routing/home-route-capture";
-import { recordHomeNativeDecision, RouteIntentStateUnreadableError } from "../../routing/route-intent-state";
-import { withDurableRouteNativeSettings } from "../../routing/route-to-live-settings";
-import { withLinkApply, LinkApplyBusyError } from "../../client/link-home";
-import { ConfigMutationLockError, withConfigMutationLockSync } from "../../config/mutation-lock";
/**
* Toggle routes for the integrations that are NOT file-merged clients.
*
@@ -68,8 +62,6 @@ import { managementInferencePort, type ManagementContext } from "./context";
export type NativeIntegrationClientId = "claude" | "grok" | "codex" | "claude-desktop";
-type NativeDecisionContext = ManagementContext & { decisionProgress?: { desiredEnabled?: boolean } };
-
/** Every reason this module can decline, in one place (audit r3 #6). */
export type NativeRefusalReason =
| "not_installed"
@@ -123,12 +115,7 @@ export interface NativeRefusalEnvelope {
/** Present on every post-commit refusal; absent only for pre-commit refusals. */
desiredEnabled?: boolean;
residualPaths?: string[];
- diagnostic?: InterceptSecretDiagnostic | NativeConfigDiagnostic;
-}
-
-interface NativeConfigDiagnostic {
- stage: "config";
- cause: "SQLITE_NOTADB" | "SQLITE_CORRUPT" | "SQLITE_CANTOPEN" | "SQLITE_IOERR" | "EACCES" | "EPERM" | "ENOENT" | "EIO" | "unknown";
+ diagnostic?: InterceptSecretDiagnostic;
}
export type NativePostCommitRefusalEnvelope = Omit & {
@@ -140,7 +127,7 @@ function refusal(
clientId: NativeIntegrationClientId,
reason: NativeRefusalReason,
message: string,
- extra: Pick = {},
+ extra: Pick