@bitkyc08/opencodex 2.83.0 → 2.84.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (216) hide show
  1. package/gui/dist/assets/App-CZtqiGcw.js +56 -0
  2. package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
  3. package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
  4. package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
  5. package/gui/dist/assets/{quota-summary-DiJO6sYY.js → quota-summary-BbEeRfPY.js} +1 -1
  6. package/gui/dist/index.html +2 -2
  7. package/package.json +2 -2
  8. package/src/adapters/coding-agent/protocol.ts +15 -8
  9. package/src/adapters/ollama-native.ts +29 -1
  10. package/src/adapters/openai-chat/messages.ts +8 -4
  11. package/src/adapters/openai-responses/empty-assistant.ts +32 -0
  12. package/src/adapters/openai-responses/passthrough.ts +3 -0
  13. package/src/adapters/openai-responses/request-strips.ts +10 -2
  14. package/src/adapters/qoder/adapter.ts +16 -14
  15. package/src/bridge/errors.ts +19 -0
  16. package/src/bridge.ts +1 -1
  17. package/src/claude/context-windows.ts +54 -1
  18. package/src/claude/intercept/connect-proxy.ts +10 -3
  19. package/src/claude/intercept/picker-ca-persistence.ts +3 -1
  20. package/src/claude/intercept/picker-listener.ts +49 -1
  21. package/src/claude/intercept/settings-no-proxy.ts +146 -0
  22. package/src/claude/intercept/settings.ts +101 -9
  23. package/src/cli/account-api.ts +65 -0
  24. package/src/cli/account-auth.ts +26 -2
  25. package/src/cli/account.ts +41 -5
  26. package/src/cli/capabilities-access-remote.ts +8 -0
  27. package/src/cli/capabilities-base.ts +13 -1
  28. package/src/cli/catalog-command-result.ts +2 -1
  29. package/src/cli/claude.ts +15 -6
  30. package/src/cli/config-command.ts +8 -2
  31. package/src/cli/debug.ts +8 -0
  32. package/src/cli/dispatch.ts +16 -3
  33. package/src/cli/export-command.ts +14 -1
  34. package/src/cli/hub-disable.ts +99 -0
  35. package/src/cli/hub.ts +4 -2
  36. package/src/cli/index.ts +13 -4
  37. package/src/cli/link-child.ts +104 -0
  38. package/src/cli/link-enrollment.ts +96 -0
  39. package/src/cli/link.ts +39 -24
  40. package/src/cli/observe-stream.ts +6 -1
  41. package/src/cli/observe.ts +10 -2
  42. package/src/cli/provider.ts +12 -0
  43. package/src/cli/registry.ts +5 -4
  44. package/src/cli/runtime-api.ts +27 -2
  45. package/src/cli/status.ts +104 -20
  46. package/src/client/connect.ts +39 -4
  47. package/src/client/hub-client.ts +14 -7
  48. package/src/client/hub-state.ts +25 -14
  49. package/src/client/link-home.ts +248 -0
  50. package/src/client/link-management-relay.ts +61 -0
  51. package/src/client/link-relay.ts +6 -1
  52. package/src/client/link-status.ts +14 -0
  53. package/src/client/machine-api.ts +14 -7
  54. package/src/client/machine-listener.ts +29 -4
  55. package/src/clients/config-export.ts +46 -16
  56. package/src/clients/omo-role-models.ts +6 -3
  57. package/src/codex/agent-role-models.ts +26 -8
  58. package/src/codex/auth-api/account-list.ts +17 -2
  59. package/src/codex/auth-api/account-selection.ts +17 -7
  60. package/src/codex/auth-api/routes.ts +5 -1
  61. package/src/codex/autostart-health.ts +3 -1
  62. package/src/codex/catalog/bundled.ts +5 -1
  63. package/src/codex/catalog/effort.ts +2 -2
  64. package/src/codex/catalog/metadata.ts +4 -2
  65. package/src/codex/catalog/native-models.ts +16 -2
  66. package/src/codex/native-profile-startup.ts +4 -1
  67. package/src/codex/prompt-journal.ts +20 -2
  68. package/src/codex/prompt-layers/encoding.ts +49 -0
  69. package/src/codex/prompt-layers/import-source.ts +30 -0
  70. package/src/codex/prompt-layers/toml-edit.ts +269 -37
  71. package/src/codex/prompt-layers/toml-read.ts +52 -4
  72. package/src/codex/prompt-layers.ts +383 -20
  73. package/src/codex/prompt-lock-claim.ts +13 -3
  74. package/src/codex/prompt-lock-evidence.ts +3 -3
  75. package/src/codex/prompt-lock-owner.ts +14 -6
  76. package/src/codex/prompt-text-probe.ts +34 -5
  77. package/src/codex/quota.ts +13 -6
  78. package/src/codex/role-auto-assign.ts +2 -5
  79. package/src/codex/routing/active-account.ts +21 -0
  80. package/src/codex/routing.ts +1 -1
  81. package/src/codex/runtime.ts +196 -8
  82. package/src/codex/shim-overlay.ts +8 -4
  83. package/src/codex/shim.ts +1 -1
  84. package/src/combos/failover.ts +3 -0
  85. package/src/config/api-key-reconcile.ts +34 -0
  86. package/src/config/diagnostics.ts +16 -0
  87. package/src/config/feature-flags.ts +5 -0
  88. package/src/config/live-reconcile.ts +43 -29
  89. package/src/config/live-topology.ts +29 -0
  90. package/src/config/load-degrade.ts +2 -1
  91. package/src/config/paths.ts +3 -0
  92. package/src/config/salvage.ts +3 -1
  93. package/src/config/schema/config-schema.ts +2 -0
  94. package/src/generated/compatibility-version.json +311 -171
  95. package/src/grok/grpc-web.ts +23 -1
  96. package/src/grok/reset-coupon-ledger.ts +75 -5
  97. package/src/grok/reset-coupons.ts +4 -2
  98. package/src/integrations/mutation-plan.ts +13 -7
  99. package/src/integrations/registry.ts +45 -11
  100. package/src/integrations/state.ts +19 -7
  101. package/src/integrations/target.ts +42 -5
  102. package/src/integrations/writer.ts +14 -6
  103. package/src/lib/app-owned-memory-stores.ts +4 -0
  104. package/src/lib/app-owned-memory.ts +11 -0
  105. package/src/lib/json-byte-size.ts +31 -0
  106. package/src/lib/optional-shutdown-hooks.ts +35 -0
  107. package/src/lib/outbound-credential-registry.ts +210 -0
  108. package/src/lib/passive-read.ts +35 -0
  109. package/src/lib/shadow-call.ts +44 -5
  110. package/src/link/codex-settings.ts +155 -0
  111. package/src/link/management-routes.ts +89 -0
  112. package/src/link/remote-probe.ts +38 -0
  113. package/src/link/ssh-argv.ts +26 -5
  114. package/src/link/ssh-runner.ts +58 -27
  115. package/src/link/status-projection.ts +22 -4
  116. package/src/link/supervisor.ts +144 -49
  117. package/src/link/tunnel-state.ts +11 -4
  118. package/src/oauth/anthropic-continuity.ts +4 -3
  119. package/src/oauth/anthropic-identity.ts +13 -2
  120. package/src/oauth/anthropic-model-quota.ts +13 -1
  121. package/src/oauth/anthropic-routing.ts +230 -23
  122. package/src/oauth/anthropic.ts +18 -4
  123. package/src/oauth/google-antigravity.ts +24 -7
  124. package/src/oauth/index.ts +24 -15
  125. package/src/oauth/kiro-account-identity.ts +106 -0
  126. package/src/oauth/kiro.ts +48 -5
  127. package/src/oauth/plan.ts +29 -0
  128. package/src/oauth/store.ts +40 -3
  129. package/src/oauth/types.ts +2 -0
  130. package/src/plugins/loader.ts +11 -0
  131. package/src/providers/anthropic-billing-ownership.ts +74 -0
  132. package/src/providers/anthropic-billing-recovery.ts +31 -0
  133. package/src/providers/anthropic-billing-refusal.ts +55 -0
  134. package/src/providers/anthropic-reset-admission.ts +62 -0
  135. package/src/providers/anthropic-reset-grant-ledger.ts +28 -6
  136. package/src/providers/api-key-resolve.ts +17 -2
  137. package/src/providers/key-failover.ts +52 -5
  138. package/src/providers/model-rename-startup.ts +5 -3
  139. package/src/providers/quota/account-cache.ts +11 -0
  140. package/src/providers/quota/anthropic-account-quota.ts +4 -2
  141. package/src/providers/quota/report-cache.ts +14 -0
  142. package/src/providers/quota/vendor-probes-oauth.ts +3 -3
  143. package/src/providers/quota.ts +4 -1
  144. package/src/providers/registry/entries-core.ts +14 -14
  145. package/src/providers/registry/entries-extended.ts +0 -1
  146. package/src/providers/registry/model-seeds.ts +17 -0
  147. package/src/providers/request-pacing.ts +9 -0
  148. package/src/providers/stale-no-vision-seed-migration.ts +72 -0
  149. package/src/responses/compaction.ts +14 -0
  150. package/src/responses/parser.ts +7 -1
  151. package/src/server/auth-cors.ts +42 -0
  152. package/src/server/configured-credentials.ts +194 -0
  153. package/src/server/index/link-management.ts +58 -0
  154. package/src/server/index/optional-listeners.ts +5 -1
  155. package/src/server/index/serve-options.ts +13 -0
  156. package/src/server/lifecycle.ts +2 -1
  157. package/src/server/management/agent-settings-routes.ts +28 -10
  158. package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
  159. package/src/server/management/codex-agent-role-routes.ts +19 -6
  160. package/src/server/management/codex-prompt-routes.ts +136 -5
  161. package/src/server/management/codex-role-auto-assign.ts +1 -1
  162. package/src/server/management/grok-coupon-routes.ts +78 -27
  163. package/src/server/management/link-enrollment.ts +192 -0
  164. package/src/server/management/link-route-state.ts +185 -0
  165. package/src/server/management/link-routes.ts +82 -402
  166. package/src/server/management/model-rows.ts +3 -1
  167. package/src/server/management/provider-overwrite-carry.ts +10 -0
  168. package/src/server/management/provider-routes.ts +1 -0
  169. package/src/server/management/route-registry.ts +5 -3
  170. package/src/server/management/shared.ts +3 -0
  171. package/src/server/management-api.ts +3 -1
  172. package/src/server/messages-native.ts +68 -42
  173. package/src/server/relay-eager.ts +2 -0
  174. package/src/server/request-log-filter.ts +6 -1
  175. package/src/server/request-log-terminal-status.ts +63 -6
  176. package/src/server/responses/adapter-dispatch.ts +232 -79
  177. package/src/server/responses/code-mode-collab-surface.ts +205 -0
  178. package/src/server/responses/codex-ws-exchange.ts +147 -6
  179. package/src/server/responses/codex-ws-wire.ts +203 -0
  180. package/src/server/responses/collaboration.ts +36 -7
  181. package/src/server/responses/compact.ts +126 -48
  182. package/src/server/responses/context-overflow.ts +13 -1
  183. package/src/server/responses/core-codex-account.ts +105 -1
  184. package/src/server/responses/core-combo-failure.ts +14 -0
  185. package/src/server/responses/core-combo.ts +7 -2
  186. package/src/server/responses/core-errors.ts +29 -1
  187. package/src/server/responses/core-options.ts +2 -0
  188. package/src/server/responses/deferred-collab-surface.ts +158 -0
  189. package/src/server/responses/deferred-send-reporter.ts +25 -0
  190. package/src/server/responses/fetch-helpers.ts +12 -1
  191. package/src/server/responses/passthrough-delivery.ts +4 -1
  192. package/src/server/responses/passthrough-dispatch.ts +15 -4
  193. package/src/server/responses/request-prepare.ts +6 -4
  194. package/src/server/responses/request-transport.ts +6 -8
  195. package/src/server/responses/sidecar-execution.ts +11 -2
  196. package/src/server/responses/ws-upstream.ts +9 -1
  197. package/src/server/startup-action-control.ts +22 -2
  198. package/src/server/startup-health-cache.ts +4 -0
  199. package/src/server/system-env-shell.ts +7 -2
  200. package/src/server/system-env.ts +11 -1
  201. package/src/service/cli.ts +5 -1
  202. package/src/service/desktop-startup.ts +147 -34
  203. package/src/service/desktop-supervision.d.mts +27 -1
  204. package/src/service/desktop-supervision.mjs +111 -5
  205. package/src/service/health-sentinel-launch.ts +176 -0
  206. package/src/service/health-sentinel-main.ts +447 -0
  207. package/src/service/health-sentinel-policy.ts +241 -0
  208. package/src/service/health-sentinel-probe.ts +54 -0
  209. package/src/service/health-sentinel-windows.ts +87 -0
  210. package/src/service/linux-linger.ts +20 -0
  211. package/src/types/config.ts +5 -0
  212. package/src/usage/expected-prices.ts +4 -0
  213. package/src/usage/log.ts +19 -1
  214. package/gui/dist/assets/App-Ba8uG696.js +0 -54
  215. package/gui/dist/assets/Tray-IImbNPRy.js +0 -1
  216. package/gui/dist/assets/index-BHRZdJuv.js +0 -94
@@ -0,0 +1,96 @@
1
+ import { createInterface } from "node:readline/promises";
2
+ import { assertSshAlias } from "../link/ssh-argv";
3
+ import { CliUsageError, RuntimeApiError, runtimeBaseUrl, rejectArgs, takeJsonFlag, takeOption, printData } from "./runtime-api";
4
+ import type { LinkCliDeps } from "./link";
5
+
6
+ const FINGERPRINT = /^SHA256:[A-Za-z0-9+/]{16,88}={0,2}$/;
7
+ const ENROLLMENT_DEADLINE_MS = 360_000;
8
+ const PHASES: Record<string, string> = {
9
+ remote_port_failed: "remote port selection", remote_port_timeout: "remote port selection",
10
+ child_preflight_failed: "Child preflight", child_preflight_timeout: "Child preflight",
11
+ child_restart_unsupported: "Child restart preflight", child_curl_missing: "Child curl preflight",
12
+ home_setup_failed: "Home setup", home_setup_timeout: "Home setup",
13
+ remote_connect_failed: "Child enrollment", remote_connect_timeout: "Child enrollment",
14
+ child_restart_failed: "Child restart", child_restart_timeout: "Child restart",
15
+ child_relay_failed: "Child relay verification", child_relay_timeout: "Child relay verification",
16
+ child_runtime_not_client: "Child runtime role verification",
17
+ remote_platform_unsupported: "remote platform verification", remote_ocx_missing: "remote OpenCodex discovery",
18
+ };
19
+ const RECOVERY = "Run ocx disconnect and ocx restart on the Child before retrying.";
20
+ type LinkRequest = <T>(path: string, init: RequestInit, deps: LinkCliDeps) => Promise<T>;
21
+
22
+ function record(value: unknown): value is Record<string, unknown> { return value !== null && typeof value === "object" && !Array.isArray(value); }
23
+ function offered(value: unknown): { alias: string; fingerprint: string; keyType: string; ocxVersion: string } {
24
+ if (!record(value) || Object.keys(value).sort().join(",") !== "alias,fingerprint,keyType,ocxVersion"
25
+ || typeof value.alias !== "string" || typeof value.fingerprint !== "string" || !FINGERPRINT.test(value.fingerprint)
26
+ || typeof value.keyType !== "string" || !/^[A-Za-z0-9@._+-]{1,64}$/.test(value.keyType)) throw new Error("Invalid host probe response");
27
+ if (typeof value.ocxVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,128})?$/.test(value.ocxVersion)) throw new Error("Invalid host version response");
28
+ return { alias: value.alias, fingerprint: value.fingerprint, keyType: value.keyType, ocxVersion: value.ocxVersion };
29
+ }
30
+
31
+ export async function runLinkEnrollment(command: "probe" | "add", args: string[], deps: LinkCliDeps, request: LinkRequest, usage: string): Promise<void> {
32
+ const wantsJson = takeJsonFlag(args);
33
+ const expected = takeOption(args, "--expect-fingerprint");
34
+ const alias = args.shift();
35
+ if (!alias) throw new CliUsageError(command + " requires an SSH alias", usage);
36
+ try { assertSshAlias(alias); } catch { throw new CliUsageError("Invalid SSH alias", usage); }
37
+ rejectArgs(args, usage);
38
+ if (command === "probe" && expected !== undefined) throw new CliUsageError("--expect-fingerprint is only for link add", usage);
39
+ if (expected !== undefined && !FINGERPRINT.test(expected)) throw new CliUsageError("Expected a SHA256 SSH fingerprint", usage);
40
+ if (command === "add" && !expected && !(deps.stdinImpl ?? process.stdin).isTTY) {
41
+ throw new CliUsageError("Non-interactive link add requires --expect-fingerprint", usage);
42
+ }
43
+ const timeout = deps.enrollmentTimeoutMs ?? ENROLLMENT_DEADLINE_MS;
44
+ if (!Number.isFinite(timeout) || timeout < 1 || timeout > ENROLLMENT_DEADLINE_MS) throw new Error("Invalid link enrollment deadline");
45
+ const controller = new AbortController();
46
+ let timer: ReturnType<typeof setTimeout> | undefined;
47
+ const expiry = new Promise<never>((_, reject) => {
48
+ timer = setTimeout(() => { controller.abort(); reject(new Error("Link enrollment timed out; its outcome is uncertain. Run ocx link status on the Home and ocx connect status on the Child before retrying.")); }, timeout);
49
+ });
50
+ let boundDeps = deps;
51
+ const post = async <T>(path: string, body: unknown): Promise<T> => {
52
+ controller.signal.throwIfAborted();
53
+ const result = await request<T>(path, { method: "POST", redirect: "error", signal: controller.signal, headers: { "content-type": "application/json" }, body: JSON.stringify(body) }, boundDeps);
54
+ controller.signal.throwIfAborted();
55
+ return result;
56
+ };
57
+ let rl: ReturnType<typeof createInterface> | undefined;
58
+ const work = async () => {
59
+ boundDeps = { ...deps, baseUrl: await runtimeBaseUrl(deps) };
60
+ controller.signal.throwIfAborted();
61
+ const probe = offered(await post<unknown>("/api/link/probe", { alias, includeVersion: true }));
62
+ if (probe.alias !== alias) throw new Error("Host probe alias did not match");
63
+ if (command === "probe") { printData(probe, wantsJson, [probe.alias + " " + probe.keyType + " " + probe.fingerprint + " opencodex " + probe.ocxVersion]); return; }
64
+ if (expected && expected !== probe.fingerprint) throw new CliUsageError("The offered host fingerprint does not match --expect-fingerprint");
65
+ if (!expected) {
66
+ let approved: boolean;
67
+ if (deps.confirmFingerprint) approved = await deps.confirmFingerprint(probe);
68
+ else {
69
+ rl = createInterface({ input: deps.stdinImpl ?? process.stdin, output: process.stderr });
70
+ approved = (await rl.question("Confirm " + alias + " " + probe.keyType + " " + probe.fingerprint + " opencodex " + probe.ocxVersion + " (type yes): ", { signal: controller.signal })).trim() === "yes";
71
+ }
72
+ controller.signal.throwIfAborted();
73
+ if (!approved) throw new CliUsageError("Host fingerprint was not confirmed");
74
+ }
75
+ const confirm = await post<unknown>("/api/link/confirm-host", { alias, fingerprint: probe.fingerprint });
76
+ if (!record(confirm) || Object.keys(confirm).sort().join(",") !== "alias,fingerprint,ocxVersion" || confirm.alias !== alias || confirm.fingerprint !== probe.fingerprint
77
+ || typeof confirm.ocxVersion !== "string" || !/^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.+-]{1,128})?$/.test(confirm.ocxVersion)) throw new Error("Invalid host confirmation response");
78
+ const result = await post<unknown>("/api/link/apply", { alias, fingerprint: probe.fingerprint });
79
+ if (!record(result) || Object.keys(result).join(",") !== "linkId" || typeof result.linkId !== "string" || !/^lnk_[0-9a-f]{16}$/.test(result.linkId)) throw new Error("Invalid link apply response");
80
+ printData({ linkId: result.linkId, alias, state: "connected", childRole: "client" }, wantsJson, ["Connected " + alias + " as a Child (" + result.linkId + ")."]);
81
+ };
82
+ try { await Promise.race([work(), expiry]); }
83
+ catch (error) {
84
+ if (error instanceof RuntimeApiError && record(error.body)) {
85
+ const code = record(error.body.error) ? error.body.error.code : undefined;
86
+ const phase = typeof code === "string" && Object.hasOwn(PHASES, code) ? PHASES[code] : undefined;
87
+ const message = phase ? "Remote Link failed during " + phase : error.message;
88
+ if (phase || error.body.residual === true) throw new Error(message + (error.body.residual === true ? ". " + RECOVERY : ""));
89
+ }
90
+ throw error;
91
+ } finally {
92
+ if (timer) clearTimeout(timer);
93
+ controller.abort();
94
+ rl?.close();
95
+ }
96
+ }
package/src/cli/link.ts CHANGED
@@ -1,3 +1,8 @@
1
+ import { clientLinkStatusIdentity } from "../client/link-status";
2
+ import { clientLinkTunnelStatus } from "../client/link-tunnel";
3
+ import { loadConfig } from "../config";
4
+ import { projectLinkStatus, type ClientLinkIdentity, type LinkStatusConfig, type LinkStatusDto } from "../link/status-projection";
5
+ import { runLinkEnrollment } from "./link-enrollment";
1
6
  import { assertSshAlias } from "../link/ssh-argv";
2
7
  import { linkStorePath } from "../link/paths";
3
8
  import { isLinkPort } from "../link/ports";
@@ -19,10 +24,13 @@ import {
19
24
  } from "./runtime-api";
20
25
 
21
26
  export const LINK_USAGE = `Usage:
27
+ ocx link probe <alias> [--json]
28
+ ocx link add <alias> [--expect-fingerprint SHA256:…] [--json]
22
29
  ocx link port [--json]
23
30
  ocx link issue --alias <alias> --tunnel-port <port> [--json]
24
31
  ocx link status [--json]
25
32
  ocx link revoke --link-id <id> [--force --yes] [--json]
33
+ ocx link reconnect --link-id <id> [--json]
26
34
 
27
35
  Forced revoke skips remote cleanup. Run ocx disconnect on the remote client.`;
28
36
 
@@ -57,7 +65,13 @@ export interface LinkIssuePayload {
57
65
  }
58
66
 
59
67
  export interface LinkCliDeps extends RuntimeApiDeps {
68
+ /** Test seam can shorten, never extend, the whole enrollment deadline. */
69
+ enrollmentTimeoutMs?: number;
60
70
  choosePort?: () => Promise<number>;
71
+ readConfig?: () => LinkStatusConfig;
72
+ readClientLinkIdentity?: (linkId: string) => ClientLinkIdentity | null;
73
+ readChildStatus?: () => LinkStatusDto["child"];
74
+ confirmFingerprint?: (host: { alias: string; fingerprint: string; keyType: string }) => Promise<boolean>;
61
75
  readStore?: () => LinkStore;
62
76
  readAdminToken?: () => string | null;
63
77
  }
@@ -172,25 +186,12 @@ function validateRevoke(value: unknown, expectedLinkId: string): void {
172
186
  if (value.linkId !== expectedLinkId) throw new Error("invalid link API response: revoke link id");
173
187
  }
174
188
 
175
- function localStatus(store: LinkStore): LinkStatusPayload {
176
- const hasLinks = store.links.length > 0;
177
- return {
178
- role: hasLinks ? "home" : "standalone",
179
- listener: {
180
- state: hasLinks && store.listenerPort !== null ? "listening" : "off",
181
- port: hasLinks ? store.listenerPort : null,
182
- },
183
- links: store.links.map(link => ({
184
- id: link.id,
185
- alias: link.alias,
186
- direction: link.direction,
187
- state: "idle",
188
- since: link.createdAt,
189
- reason: null,
190
- tunnelPort: link.tunnelPort,
191
- })),
192
- child: null,
193
- };
189
+ function localStatus(store: LinkStore, deps: LinkCliDeps): LinkStatusPayload {
190
+ const config = (deps.readConfig ?? loadConfig)();
191
+ const identity = config.client?.transport === "link" && config.client.link
192
+ ? (deps.readClientLinkIdentity ?? clientLinkStatusIdentity)(config.client.link.linkId) : null;
193
+ return projectLinkStatus(store, [], { state: store.links.length > 0 && store.listenerPort !== null ? "listening" : "off", port: store.links.length > 0 ? store.listenerPort : null },
194
+ config, { version: 1, entries: {} }, (deps.readChildStatus ?? clientLinkTunnelStatus)(), identity);
194
195
  }
195
196
 
196
197
  async function linkRequest<T>(path: string, init: RequestInit, deps: LinkCliDeps): Promise<T> {
@@ -211,10 +212,10 @@ async function linkRequest<T>(path: string, init: RequestInit, deps: LinkCliDeps
211
212
  }
212
213
  }
213
214
 
214
- function errorCodeBody(body: unknown): { error: { code: string } } | null {
215
+ function errorCodeBody(body: unknown): { error: { code: string }; residual?: true } | null {
215
216
  if (!isRecord(body) || !isRecord(body.error)) return null;
216
217
  const code = body.error.code;
217
- return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code } } : null;
218
+ return typeof code === "string" && /^[a-z_]{1,64}$/.test(code) ? { error: { code }, ...(body.residual === true ? { residual: true as const } : {}) } : null;
218
219
  }
219
220
 
220
221
  async function runPort(args: string[], deps: LinkCliDeps): Promise<void> {
@@ -252,7 +253,7 @@ async function runStatus(args: string[], deps: LinkCliDeps): Promise<void> {
252
253
  } catch (error) {
253
254
  if (!deps.baseUrl && error instanceof RuntimeApiError && error.status === 503) {
254
255
  const store = (deps.readStore ?? (() => readLinkStore(linkStorePath())))();
255
- console.log(JSON.stringify(localStatus(store)));
256
+ console.log(JSON.stringify(localStatus(store, deps)));
256
257
  return;
257
258
  }
258
259
  throw error;
@@ -303,14 +304,28 @@ async function runRevoke(args: string[], deps: LinkCliDeps): Promise<void> {
303
304
  console.log(JSON.stringify({ linkId }));
304
305
  }
305
306
 
307
+ async function runReconnect(args: string[], deps: LinkCliDeps): Promise<void> {
308
+ takeJsonFlag(args);
309
+ const linkId = takeOption(args, "--link-id");
310
+ if (!linkId || !LINK_ID.test(linkId)) throw new CliUsageError("reconnect requires a valid --link-id", LINK_USAGE);
311
+ rejectArgs(args, LINK_USAGE);
312
+ const response = await linkRequest<unknown>(`/api/link/${encodeURIComponent(linkId)}/reconnect`, { method: "POST" }, deps);
313
+ if (!isRecord(response)) throw new Error("invalid link API response: reconnect object");
314
+ assertExactKeys(response, ["linkId"], "reconnect");
315
+ if (response.linkId !== linkId) throw new Error("invalid link API response: reconnect link id");
316
+ console.log(JSON.stringify({ linkId }));
317
+ }
318
+
306
319
  export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}): Promise<number> {
307
320
  return runCliAction(async () => {
308
321
  const args = [...rawArgs];
309
322
  const command = args.shift();
310
- if (command === "port") await runPort(args, deps);
323
+ if (command === "probe" || command === "add") await runLinkEnrollment(command, args, deps, linkRequest, LINK_USAGE);
324
+ else if (command === "port") await runPort(args, deps);
311
325
  else if (command === "issue") await runIssue(args, deps);
312
326
  else if (command === "status") await runStatus(args, deps);
313
327
  else if (command === "revoke") await runRevoke(args, deps);
314
- else throw new CliUsageError("link requires one of: port, issue, status, revoke", LINK_USAGE);
328
+ else if (command === "reconnect") await runReconnect(args, deps);
329
+ else throw new CliUsageError("link requires one of: probe, add, port, issue, status, revoke, reconnect", LINK_USAGE);
315
330
  });
316
331
  }
@@ -89,7 +89,12 @@ export async function withObserveStream(
89
89
  // Snapshot recovery comes from our observed target, never arbitrary transport text.
90
90
  if (context.kind === "snapshot") {
91
91
  if (!found) throw new ObservationError("Proxy is not running. Start it with: ocx start");
92
- if (found.role === "client") throw new ObservationError("This listener runs in the client role and has no management API. Run this command on its serving hub, or open the hub dashboard.");
92
+ if (found.role === "client") {
93
+ const { readLinkChildContext, linkChildRefusal } = await import("./link-child");
94
+ const link = readLinkChildContext();
95
+ throw new ObservationError(link ? linkChildRefusal(link)
96
+ : "This listener runs in the client role and has no management API. Run this command on its serving hub, or open the hub dashboard.");
97
+ }
93
98
  }
94
99
  return found;
95
100
  } }), signal);
@@ -113,8 +113,12 @@ async function logs(argv: string[], deps: ObserveStreamDeps): Promise<number> {
113
113
  return followLogs({ query: new URLSearchParams(query(params)), limit, jsonl: wantsJsonl, events: wantsEvents, formatRow: formatLog }, deps);
114
114
  }
115
115
  const data = await runtimeRequest(`/api/logs${query(params)}`, {}, deps);
116
+ const rows = logRows(data);
117
+ if (!wantsJson && !wantsJsonl && rows.length === 0 && [provider, model, status, conversationId, account].some(Boolean)) {
118
+ console.log("No log entry matched the filters.");
119
+ }
116
120
  if (wantsJson) printData(data, true);
117
- else for (const row of logRows(data)) {
121
+ else for (const row of rows) {
118
122
  if (wantsJsonl) console.log(JSON.stringify(row));
119
123
  else console.log(formatLog(row));
120
124
  }
@@ -206,8 +210,12 @@ async function usage(argv: string[], deps: RuntimeApiDeps): Promise<void> {
206
210
  if (token.kind !== "present" || token.fingerprint !== connection.value.tokenFingerprint) {
207
211
  throw new Error("Client usage unavailable: the enrolled data key is missing or changed; repair the client connection");
208
212
  }
213
+ // A Remote Link Child sends this read through the connection-bound link relay (#6957).
214
+ const link = connection.value.transport === "link" && connection.value.link;
215
+ const peerFetch = deps.fetchImpl
216
+ ?? (link ? (await import("../client/link-home")).linkBoundFetch(connection.value, token.token) : undefined);
209
217
  result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix), {
210
- fetchImpl: deps.fetchImpl, timeoutMs: 60_000,
218
+ fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
211
219
  });
212
220
  const current = readClientConnectionState();
213
221
  const currentToken = readServiceApiTokenState();
@@ -584,8 +584,19 @@ export async function handleProviderCommand(args: string[], deps: ProviderComman
584
584
  return;
585
585
  }
586
586
 
587
+ // #6957: a link Child's local providers are inactive; changing them is refused like any other
588
+ // provider change, which belongs to the Home.
589
+ if (sub === "add" || sub === "remove" || sub === "set-default") {
590
+ const refusal = (await import("./link-child")).linkChildProviderChangeRefusal(`ocx provider ${sub}`);
591
+ if (refusal) {
592
+ console.error(refusal);
593
+ process.exitCode = 1;
594
+ return;
595
+ }
596
+ }
587
597
  switch (sub) {
588
598
  case "list":
599
+ await (await import("./link-child")).printLinkChildLocalNotice("The providers");
589
600
  handleList(subArgs);
590
601
  break;
591
602
  case "add":
@@ -595,6 +606,7 @@ export async function handleProviderCommand(args: string[], deps: ProviderComman
595
606
  handleRemove(subArgs);
596
607
  break;
597
608
  case "show":
609
+ await (await import("./link-child")).printLinkChildLocalNotice("The provider settings");
598
610
  handleShow(subArgs);
599
611
  break;
600
612
  case "set-default":
@@ -144,14 +144,15 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
144
144
  },
145
145
  {
146
146
  name: "link",
147
- usage: "ocx link <port|issue|status|revoke>",
147
+ usage: "ocx link <port|issue|status|revoke|reconnect>",
148
148
  summary: "Allocate and manage a loopback remote home link.",
149
149
  details: [
150
150
  "Port: ocx link port [--json]",
151
151
  "Issue: ocx link issue --alias <alias> --tunnel-port <port> [--json]",
152
152
  "Status: ocx link status [--json]",
153
153
  "Revoke: ocx link revoke --link-id <id> [--force --yes] [--json]",
154
- "Issue, status, and revoke use the running proxy's loopback management API and admin token.",
154
+ "Reconnect: ocx link reconnect --link-id <id> [--json]",
155
+ "Issue, status, revoke, and reconnect use the running proxy's loopback management API and admin token.",
155
156
  ],
156
157
  },
157
158
  {
@@ -245,8 +246,8 @@ export const CLI_COMMANDS: CliCommandEntry[] = [
245
246
  },
246
247
  {
247
248
  name: "hub",
248
- usage: "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]",
249
- summary: "Hub-side commands. `invite` prints a ready-to-run `ocx connect` line for one more machine.",
249
+ usage: "ocx hub invite [--json] [--data-url <origin>] [--management-url <origin>] [--clients codex,claude]\n ocx hub disable [--revoke-clients --client-key <id> ...] [--json]",
250
+ summary: "Invite another machine, or disable hub mode in one validated config write.",
250
251
  details: [
251
252
  "Topology: a hub serves ONE port. Remote machines dial `hostname:port` with their own per-client",
252
253
  "key; the hub's own local processes dial `127.0.0.1:<same port>` with no credential, through the",
@@ -12,6 +12,7 @@
12
12
  import { findLiveProxy, probeHostname, type LivenessIo, type LiveProxy } from "../server/proxy-liveness";
13
13
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
14
14
  import { redactSecretArgs } from "./secret-args";
15
+ import type { LinkHomeDeps } from "../client/link-home";
15
16
  export { redactSecretArgs };
16
17
 
17
18
  export type CliStdin = NodeJS.ReadableStream & { isTTY?: boolean; readableEnded?: boolean };
@@ -28,6 +29,8 @@ export interface RuntimeApiDeps {
28
29
  stdinSignal?: AbortSignal;
29
30
  /** Optional proxy liveness probe injection for commands that check or fall back around live runtime state. */
30
31
  findLiveProxy?: (io?: LivenessIo) => Promise<LiveProxy | null>;
32
+ /** Remote Link Child: how allowlisted reads reach the Home (#6957). */
33
+ linkHome?: LinkHomeDeps;
31
34
  }
32
35
 
33
36
  export class CliUsageError extends Error {
@@ -45,7 +48,7 @@ export class RuntimeApiError extends Error {
45
48
  readonly status: number,
46
49
  readonly body: unknown,
47
50
  /** Local discovery refusals occur before the management request is sent. */
48
- readonly code?: "proxy_not_running" | "client_role_management_unavailable",
51
+ readonly code?: "proxy_not_running" | "client_role_management_unavailable" | "link_child_management_unavailable",
49
52
  ) {
50
53
  super(message);
51
54
  this.name = "RuntimeApiError";
@@ -83,6 +86,10 @@ export async function runtimeBaseUrl(deps: RuntimeApiDeps = {}): Promise<string>
83
86
  // this costs no extra request. Only the client role is refused: an absent role is a
84
87
  // standalone or hub proxy (or a legacy body that predates the field), and both serve /api/*.
85
88
  if (live.role === "client") {
89
+ // A Remote Link Child has no relay transport to point at; it names its Home instead (#6957).
90
+ const { readLinkChildContext, linkChildRefusal } = await import("./link-child");
91
+ const link = readLinkChildContext();
92
+ if (link) throw new RuntimeApiError(linkChildRefusal(link), 503, null, "link_child_management_unavailable");
86
93
  throw new RuntimeApiError(clientRoleManagementRefusal(live.port), 503, null, "client_role_management_unavailable");
87
94
  }
88
95
  return `http://${probeHostname(live.hostname)}:${live.port}`;
@@ -180,7 +187,25 @@ export async function runtimeRequest<T = unknown>(
180
187
  init: RequestInit = {},
181
188
  deps: RuntimeApiDeps = {},
182
189
  ): Promise<T> {
183
- const baseUrl = await runtimeBaseUrl(deps);
190
+ let baseUrl: string;
191
+ try {
192
+ baseUrl = await runtimeBaseUrl(deps);
193
+ } catch (error) {
194
+ if (!(error instanceof RuntimeApiError) || error.code !== "link_child_management_unavailable") throw error;
195
+ // #6957: on a Remote Link Child an allowlisted read is answered by the Home over the link;
196
+ // every other request is refused with the operation named, before anything is sent.
197
+ const method = (init.method ?? "GET").toUpperCase();
198
+ const { readLinkChildContext, linkChildReadable, linkChildRefusal, readLinkHomeJson } = await import("./link-child");
199
+ const link = readLinkChildContext();
200
+ if (!link) throw error;
201
+ const pathname = new URL(path.startsWith("/") ? path : `/${path}`, "http://cli.invalid").pathname;
202
+ if (!linkChildReadable(method, path)) {
203
+ throw new RuntimeApiError(linkChildRefusal(link, `${method} ${pathname}`), 503, null, "link_child_management_unavailable");
204
+ }
205
+ const { status, body } = await readLinkHomeJson(link, path, deps.linkHome);
206
+ if (status < 200 || status >= 300) throw new RuntimeApiError(responseMessage(body, status), status, body);
207
+ return body as T;
208
+ }
184
209
  const headers = runningProxyUpdateHeaders();
185
210
  for (const [key, value] of new Headers(init.headers).entries()) headers.set(key, value);
186
211
  const fetchImpl = deps.fetchImpl ?? fetch;
package/src/cli/status.ts CHANGED
@@ -5,7 +5,7 @@ import { existsSync, readFileSync } from "node:fs";
5
5
  import { codexAutoStartEnabled, getConfigPath, readConfigDiagnostics } from "../config";
6
6
  import { getPidPath, readPid, readRuntimePort, type RuntimePortState } from "../config/process-state";
7
7
  import { diagnoseCodexBundledPlugins, type CodexPluginsDiagnostic } from "../codex/plugins-doctor";
8
- import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
8
+ import { findLiveProxy, probeHostname, type LiveProxy } from "../server/proxy-liveness";
9
9
  import type { OcxConfig } from "../types";
10
10
  import { diagnoseService, serviceLogPath } from "../service";
11
11
  import { inspectDesktopSupervision } from "../service/desktop-supervision.mjs";
@@ -104,6 +104,16 @@ export type CliRemoteHubStatus = {
104
104
  truncated: boolean;
105
105
  /** The hub's own Claude Code toggle; a client cannot infer it from local config. */
106
106
  claudeCodeEnabled: boolean | null;
107
+ /**
108
+ * Present on a Remote Link Child (#6957): the peer is a Home reached over the link, not a hub,
109
+ * and every sentence names it that way. `codexSettings` lists the Home's Codex settings the
110
+ * last sync applied, or why the Child's local values are in effect.
111
+ */
112
+ link?: {
113
+ linkId: string;
114
+ homePhrase: string;
115
+ codexSettings: { source: "home"; fetchedAt: string; lines: string[] } | { source: "local"; reason: string };
116
+ };
107
117
  };
108
118
 
109
119
  export type CliStatusJson = {
@@ -152,6 +162,7 @@ export type CliStatusJson = {
152
162
  };
153
163
  connection: {
154
164
  state: "disconnected" | "connected" | "invalid" | "mismatched";
165
+ transport?: "hub" | "link";
155
166
  reason?: string;
156
167
  serverUrl?: string;
157
168
  managementUrl?: string;
@@ -321,8 +332,12 @@ export function selectStatusStartupHealth(
321
332
  if (evidence?.kind === "desktop" && evidence.runtimePid === livePid) {
322
333
  const local = fallback();
323
334
  const supervisor = local.desktop?.supervisor;
324
- // The local probe runs later; it must still describe the same unowned process pair.
325
- if (local.desktop?.owned === false && supervisor
335
+ // The local probe runs later; it must still describe the same process pair. macOS and Linux
336
+ // accept only an unowned pair. On Windows an older runtime never published a Desktop
337
+ // diagnostic, so a durable Desktop claim beside proven live parentage qualifies too.
338
+ const ownershipEligible = local.desktop?.owned === false
339
+ || (local.desktop?.owned === true && local.platform === "win32");
340
+ if (ownershipEligible && supervisor
326
341
  && supervisor.runtimePid === evidence.runtimePid && supervisor.supervisorPid === evidence.supervisorPid) {
327
342
  return { startup: local, startupSource: "local-supervision-override" };
328
343
  }
@@ -499,8 +514,8 @@ function boundHubStateToken(
499
514
  * degradation.
500
515
  */
501
516
  export async function collectRemoteHubStatus(
502
- connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt">,
503
- options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number } = {},
517
+ connection: Pick<ClientConnectionStatus, "state" | "serverUrl" | "apiKeyId" | "connectedAt"> & { transport?: "hub" | "link" },
518
+ options: { fetchImpl?: typeof fetch; timeoutMs?: number; now?: number; findChild?: () => Promise<LiveProxy | null> } = {},
504
519
  ): Promise<CliRemoteHubStatus> {
505
520
  if (connection.state !== "connected" || !connection.serverUrl || !connection.apiKeyId || !connection.connectedAt) {
506
521
  return disconnectedRemoteHubStatus();
@@ -512,14 +527,50 @@ export async function collectRemoteHubStatus(
512
527
  connectedAt: connection.connectedAt,
513
528
  };
514
529
  const bound = boundHubStateToken(readClientConnectionState(), readServiceApiTokenState(), owner);
530
+ // Link credentials never go to the persisted tunnel URL from this CLI. Ask the local Child
531
+ // without a credential; its connection-bound relay authenticates the actual Home connection.
532
+ let fetchImpl = options.fetchImpl;
533
+ let allowNetwork = true;
534
+ let localRelayFetchImpl: typeof fetch | undefined;
535
+ if (connection.transport === "link") {
536
+ const live = await (options.findChild ?? findLiveProxy)();
537
+ const current = readClientConnectionState();
538
+ allowNetwork = live?.role === "client" && current.kind === "connected" && sameClientConnectionOwner(current.value, owner);
539
+ if (allowNetwork && live) {
540
+ const localOrigin = `http://${probeHostname(live.hostname)}:${live.port}`;
541
+ localRelayFetchImpl = ((_, init) => (options.fetchImpl ?? fetch)(localOrigin + "/v1/hub-state", {
542
+ method: "GET", redirect: "manual", signal: init?.signal, headers: { Accept: "application/json" },
543
+ })) as typeof fetch;
544
+ }
545
+ }
546
+ // #6957: the Home's name, the link id and the Codex settings the last sync applied.
547
+ const linkState = readClientConnectionState();
548
+ const linkConnection = connection.transport === "link" && linkState.kind === "connected" && linkState.value.transport === "link" && linkState.value.link
549
+ ? linkState.value : null;
550
+ const linkHome = linkConnection ? await import("../client/link-home") : null;
515
551
  const resolved = await resolveHubState({
516
552
  owner,
517
- token: bound.token,
553
+ token: connection.transport === "link" ? null : bound.token,
554
+ ...(localRelayFetchImpl ? { localRelayFetchImpl } : {}),
518
555
  ...(bound.withheldReason ? { withheldTokenReason: bound.withheldReason } : {}),
519
- ...(options.fetchImpl ? { fetchImpl: options.fetchImpl } : {}),
556
+ ...(fetchImpl ? { fetchImpl } : {}),
557
+ allowNetwork,
558
+ ...(connection.transport === "link" ? { peer: "Home" as const } : {}),
520
559
  ...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
521
560
  ...(options.now === undefined ? {} : { now: options.now }),
522
561
  });
562
+ let link: CliRemoteHubStatus["link"];
563
+ if (linkConnection && linkHome) {
564
+ const { describeHomeCodexSettings } = await import("../link/codex-settings");
565
+ const record = linkHome.readHomeCodexSettingsRecord(linkConnection.link!.linkId);
566
+ link = {
567
+ linkId: linkConnection.link!.linkId,
568
+ homePhrase: linkHome.linkHomePhrase(linkConnection),
569
+ codexSettings: record
570
+ ? { source: "home", fetchedAt: record.fetchedAt, lines: describeHomeCodexSettings(record.document.settings) }
571
+ : { source: "local", reason: "no Home settings recorded yet; run: ocx sync" },
572
+ };
573
+ }
523
574
  return {
524
575
  connected: true,
525
576
  // The hub's own advertised origin when it publishes one; otherwise the URL this client
@@ -535,6 +586,7 @@ export async function collectRemoteHubStatus(
535
586
  subagentModels: resolved.state?.subagentModels ?? [],
536
587
  truncated: resolved.state?.truncated === true,
537
588
  claudeCodeEnabled: resolved.state ? resolved.state.claudeCode.enabled : null,
589
+ ...(link ? { link } : {}),
538
590
  };
539
591
  }
540
592
 
@@ -546,8 +598,24 @@ export async function collectRemoteHubStatus(
546
598
  * A buried `Remote hub: connected (<url>)` line — which is all this report had — does not stop
547
599
  * that, as #4236 demonstrated.
548
600
  */
549
- export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | null {
601
+ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string | null {
550
602
  if (!remoteHub.connected) return null;
603
+ if (remoteHub.link) {
604
+ const peer = `${remoteHub.link.homePhrase} over Remote Link (${remoteHub.link.linkId})`;
605
+ if (remoteHub.stateSource === "unavailable") {
606
+ return `⚠️ ${peer}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the Home.`;
607
+ }
608
+ const cached = remoteHub.stateSource === "cache"
609
+ ? ` — cached ${remoteHub.ageSeconds ?? "?"}s ago (${remoteHub.reason ?? "live read failed"})`
610
+ : "";
611
+ return `🔗 State from ${peer}${cached}: provider credentials, logins and delegable models below are the Home's, not this machine's.`;
612
+ }
613
+ if (transport === "link") {
614
+ const origin = remoteHub.origin ?? "unknown";
615
+ return remoteHub.stateSource === "unavailable"
616
+ ? `🔗 Remote Link Home ${origin}: provider credentials and routing come from Home. State unavailable (${remoteHub.reason ?? "unknown reason"}).`
617
+ : `🔗 State from Remote Link Home ${origin}${remoteHub.stateSource === "cache" ? " (cached)" : ""}: provider credentials, logins and models below come from Home.`;
618
+ }
551
619
  const origin = remoteHub.origin ?? "the hub";
552
620
  if (remoteHub.stateSource === "unavailable") {
553
621
  return `⚠️ Hub ${origin}: state unavailable (${remoteHub.reason ?? "unknown reason"}) — provider and login lines below are LOCAL and do not describe the hub.`;
@@ -562,32 +630,47 @@ export function remoteHubBannerLine(remoteHub: CliRemoteHubStatus): string | nul
562
630
  * The hub-sourced provider/login/model block, owned here so the sentences are testable without
563
631
  * spawning the CLI. Indentation is the caller's. Empty when nothing true is known.
564
632
  */
565
- export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus): string[] {
566
- if (!remoteHub.connected || remoteHub.stateSource === "unavailable") return [];
567
- const origin = remoteHub.origin ?? "hub";
633
+ export function remoteHubStatusLines(remoteHub: CliRemoteHubStatus, transport?: "hub" | "link"): string[] {
634
+ if (!remoteHub.connected) return [];
635
+ const settings = remoteHub.link ? linkCodexSettingsLines(remoteHub.link) : [];
636
+ if (remoteHub.stateSource === "unavailable") return settings;
637
+ const isLink = Boolean(remoteHub.link) || transport === "link";
638
+ const peer = remoteHub.link?.homePhrase ?? `${isLink ? "Home" : "hub"} ${remoteHub.origin ?? (isLink ? "Home" : "hub")}`;
639
+ const subject = isLink ? "the Home" : "the hub";
568
640
  const lines = [
569
- `OAuth logins (hub ${origin}):`,
641
+ `OAuth logins (${peer}):`,
570
642
  ...(remoteHub.oauth.length === 0
571
- ? [" (the hub reported no OAuth providers)"]
643
+ ? [` (${subject} reported no OAuth providers)`]
572
644
  : remoteHub.oauth.map(entry => ` ${entry.provider.padEnd(10)} ${entry.loggedIn ? "✓ logged in" : "✗ not logged in"}`)),
573
- `Providers (hub ${origin}):`,
645
+ `Providers (${peer}):`,
574
646
  ...(remoteHub.providers.length === 0
575
- ? [" (the hub reported no providers)"]
647
+ ? [` (${subject} reported no providers)`]
576
648
  : remoteHub.providers.map(provider => {
577
649
  // `authMode` is what keeps "no API key" from reading as "not configured": an `oauth`
578
650
  // provider legitimately has no key and is still fully usable.
579
651
  const credential = provider.hasCredential ? "credential stored" : `no API key (authMode ${provider.authMode ?? "key"})`;
580
652
  return ` ${provider.name.padEnd(10)} ${provider.adapter} — ${credential}${provider.disabled ? ", disabled" : ""}`;
581
653
  })),
582
- `Delegable models (hub ${origin}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
654
+ `Delegable models (${peer}): ${remoteHub.subagentModels.length === 0 ? "none" : remoteHub.subagentModels.join(", ")}`,
583
655
  ];
584
- if (remoteHub.hubVersion) lines.push(`Hub version: ${remoteHub.hubVersion}`);
656
+ if (remoteHub.hubVersion) lines.push(`${remoteHub.link ? "Home" : "Hub"} version: ${remoteHub.hubVersion}`);
585
657
  // The hub told us its own lists are a prefix. Saying nothing here would make this report claim
586
658
  // completeness it does not have — the same shape of confident-and-wrong answer as #4236.
587
659
  if (remoteHub.truncated) {
588
- lines.push(" (the hub truncated this state to fit its response caps; some rows are not listed)");
660
+ lines.push(` (${subject} truncated this state to fit its response caps; some rows are not listed)`);
589
661
  }
590
- return lines;
662
+ return [...lines, ...settings];
663
+ }
664
+
665
+ /** Which Codex settings the last sync took from the Home (#6957). */
666
+ export function linkCodexSettingsLines(link: NonNullable<CliRemoteHubStatus["link"]>): string[] {
667
+ if (link.codexSettings.source === "local") {
668
+ return [`Codex settings: this machine's local values (${link.codexSettings.reason})`];
669
+ }
670
+ return [
671
+ `Codex settings from ${link.homePhrase} (applied ${link.codexSettings.fetchedAt}):`,
672
+ ...link.codexSettings.lines.map(line => ` ${line}`),
673
+ ];
591
674
  }
592
675
 
593
676
  export function selectListenTarget(
@@ -760,7 +843,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
760
843
  // Asked before the local probes below so a connected client's report is hub-sourced from its
761
844
  // first line. Bounded and failure-tolerant: an offline hub degrades the remoteHub block, it
762
845
  // does not fail `ocx status`.
763
- const remoteHub = await collectRemoteHubStatus(clientConnection);
846
+ const remoteHub = await collectRemoteHubStatus({ ...clientConnection, transport: config.client?.transport });
764
847
  // Prefer identity-verified liveness (runtime-port + /healthz) over ocx.pid alone (#618).
765
848
  // Pass the already-resolved diagnostics config so findLiveProxy does not re-load and
766
849
  // warn on malformed config.json (status --json must stay stderr-clean).
@@ -960,6 +1043,7 @@ export async function collectStatus(options: { mainAccountPolicy?: boolean } = {
960
1043
  },
961
1044
  connection: {
962
1045
  state: clientConnection.state,
1046
+ ...(config.client?.transport ? { transport: config.client.transport } : {}),
963
1047
  ...(clientConnection.reason ? { reason: clientConnection.reason } : {}),
964
1048
  ...(clientConnection.serverUrl ? { serverUrl: clientConnection.serverUrl } : {}),
965
1049
  ...(clientConnection.managementUrl ? { managementUrl: clientConnection.managementUrl } : {}),