@bitkyc08/opencodex 2.90.0 → 2.91.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 (132) hide show
  1. package/gui/dist/assets/App-DMps4OLk.css +1 -0
  2. package/gui/dist/assets/{App-DgxkE6Md.js → App-nZ34cR5G.js} +18 -18
  3. package/gui/dist/assets/{Tray-DbTNc_88.js → Tray-BuLy7hL1.js} +1 -1
  4. package/gui/dist/assets/{index-TmaHa54u.js → index-DGXS51sp.js} +12 -12
  5. package/gui/dist/assets/{quota-summary-B16x5DZZ.js → quota-summary-DO1Sx5TM.js} +1 -1
  6. package/gui/dist/index.html +1 -1
  7. package/package.json +1 -1
  8. package/src/adapters/anthropic.ts +2 -5
  9. package/src/adapters/claude-cli/usage-admission.ts +14 -0
  10. package/src/adapters/openai-responses/passthrough.ts +2 -1
  11. package/src/adapters/openai-responses/request-strips.ts +32 -0
  12. package/src/adapters/openai-responses/transport.ts +6 -0
  13. package/src/chat/inbound.ts +20 -2
  14. package/src/chat/outbound.ts +36 -4
  15. package/src/chat/reasoning-details.ts +137 -0
  16. package/src/cli/capabilities-agents-routing.ts +50 -0
  17. package/src/cli/capabilities-base.ts +15 -2
  18. package/src/cli/capabilities-observe-system.ts +4 -4
  19. package/src/cli/codex-shim-autorestore.ts +5 -0
  20. package/src/cli/dispatch.ts +30 -0
  21. package/src/cli/fallback.ts +127 -0
  22. package/src/cli/help.ts +1 -0
  23. package/src/cli/index.ts +1 -2
  24. package/src/cli/link-apply-restart.ts +72 -0
  25. package/src/cli/link-child.ts +6 -2
  26. package/src/cli/link-cleanup.ts +95 -0
  27. package/src/cli/link-enrollment.ts +4 -0
  28. package/src/cli/link.ts +84 -9
  29. package/src/cli/provider.ts +44 -2
  30. package/src/cli/registry.ts +13 -1
  31. package/src/cli/restart-failure.ts +4 -0
  32. package/src/cli/runtime-api.ts +1 -1
  33. package/src/cli/status-codex-settings.ts +56 -0
  34. package/src/cli/status-fallback.ts +29 -0
  35. package/src/cli/status-install.ts +52 -0
  36. package/src/cli/status.ts +20 -1
  37. package/src/cli/system-restart-client.ts +32 -1
  38. package/src/cli/update-restart-child.ts +4 -3
  39. package/src/cli/update-restart-home.ts +20 -8
  40. package/src/client/connect.ts +82 -6
  41. package/src/client/home-unreachable.ts +16 -0
  42. package/src/client/hub-relay.ts +8 -2
  43. package/src/client/link-home.ts +119 -6
  44. package/src/client/link-parity-reporter.ts +107 -0
  45. package/src/client/link-relay-transport.ts +8 -1
  46. package/src/client/link-relay.ts +27 -3
  47. package/src/client/machine-listener.ts +83 -1
  48. package/src/client/runtime-restart.ts +127 -0
  49. package/src/client/runtime.ts +137 -25
  50. package/src/codex/auth-context.ts +10 -6
  51. package/src/codex/catalog/parsing.ts +4 -0
  52. package/src/codex/desktop-compatibility/windows-key-protection.ts +18 -6
  53. package/src/codex/quota-history.ts +7 -0
  54. package/src/codex/quota.ts +10 -0
  55. package/src/codex/subagent-model-fallback.ts +24 -1
  56. package/src/combos/index.ts +1 -0
  57. package/src/combos/resolve.ts +12 -0
  58. package/src/config/diagnostics.ts +5 -0
  59. package/src/config/load-degrade.ts +2 -0
  60. package/src/config/schema/config-schema.ts +3 -0
  61. package/src/config/schema/fallback-policy.ts +40 -0
  62. package/src/generated/compatibility-version.json +205 -97
  63. package/src/lib/authorization-scheme.ts +95 -0
  64. package/src/lib/provider-client-headers.ts +1 -0
  65. package/src/lib/redact.ts +2 -2
  66. package/src/lib/system-restart-contract.ts +24 -0
  67. package/src/lib/test-home-guard.ts +78 -11
  68. package/src/link/child-parity.ts +124 -0
  69. package/src/link/codex-settings.ts +37 -0
  70. package/src/link/compatibility.ts +119 -0
  71. package/src/link/management-routes.ts +2 -0
  72. package/src/link/peer-report.ts +100 -0
  73. package/src/link/remote-platform.ts +78 -0
  74. package/src/link/ssh-argv.ts +19 -0
  75. package/src/link/status-projection.ts +24 -2
  76. package/src/oauth/index.ts +1 -1
  77. package/src/oauth/kimi.ts +2 -0
  78. package/src/oauth/nous.ts +4 -1
  79. package/src/oauth/store.ts +2 -1
  80. package/src/protocols/codecs/chat.ts +3 -3
  81. package/src/protocols/encoders/adapter-events.ts +26 -2
  82. package/src/protocols/encoders/chat.ts +55 -3
  83. package/src/providers/mistral-transport.ts +37 -4
  84. package/src/providers/quota/antigravity.ts +2 -8
  85. package/src/quota-pool/assemble.ts +326 -0
  86. package/src/quota-pool/contract.ts +58 -0
  87. package/src/quota-pool/identity.ts +203 -0
  88. package/src/remote/features.ts +74 -0
  89. package/src/remote/protocol.ts +2 -0
  90. package/src/remote/served-by.ts +57 -0
  91. package/src/responses/parser.ts +7 -0
  92. package/src/responses/reasoning-envelope.ts +12 -0
  93. package/src/routing/fallback-policy.ts +290 -0
  94. package/src/routing/fallback-state.ts +179 -0
  95. package/src/server/audio-client.ts +2 -1
  96. package/src/server/audio-dictation.ts +2 -1
  97. package/src/server/audio-upstream.ts +2 -1
  98. package/src/server/auth-cors.ts +6 -5
  99. package/src/server/background-lifecycle.ts +4 -0
  100. package/src/server/chat-completions.ts +8 -1
  101. package/src/server/gui-session.ts +2 -1
  102. package/src/server/index/link-listener.ts +11 -1
  103. package/src/server/index/link-management.ts +47 -0
  104. package/src/server/index/link-relay-sessions.ts +11 -2
  105. package/src/server/index/optional-listeners.ts +7 -2
  106. package/src/server/index/serve-options.ts +19 -2
  107. package/src/server/live.ts +5 -0
  108. package/src/server/management/context.ts +1 -1
  109. package/src/server/management/fallback-routes.ts +75 -0
  110. package/src/server/management/link-enrollment.ts +21 -3
  111. package/src/server/management/link-route-state.ts +2 -0
  112. package/src/server/management/link-routes.ts +12 -1
  113. package/src/server/management/quota-pool-routes.ts +24 -0
  114. package/src/server/management/route-registry.ts +3 -0
  115. package/src/server/management-api.ts +16 -0
  116. package/src/server/management-auth.ts +2 -1
  117. package/src/server/responses/agent-task-recovery.ts +1 -1
  118. package/src/server/responses/compact.ts +2 -2
  119. package/src/server/responses/core-auth.ts +11 -1
  120. package/src/server/responses/core-combo.ts +72 -1
  121. package/src/server/responses/encrypted-payload.ts +3 -3
  122. package/src/server/responses/main-hard-lock-routing.ts +54 -2
  123. package/src/server/responses/policy-fallback.ts +44 -1
  124. package/src/server/responses/request-prepare.ts +9 -1
  125. package/src/server/restart-replacement.ts +50 -2
  126. package/src/server/ws-bridge.ts +5 -0
  127. package/src/service/cli.ts +25 -2
  128. package/src/service/diagnostics.ts +20 -6
  129. package/src/service/status-json.ts +116 -0
  130. package/src/types/config.ts +7 -0
  131. package/src/types/fallback.ts +38 -0
  132. package/gui/dist/assets/App-dnkJ85te.css +0 -1
@@ -32,6 +32,7 @@ import {
32
32
  startMemoryWatchdog,
33
33
  type MemoryWatchdog,
34
34
  } from "./memory-watchdog";
35
+ import { ensureQuotaPoolIdentity } from "../quota-pool/identity";
35
36
 
36
37
  type PolicyApply = (policy: StorageCleanupPolicy) => void;
37
38
 
@@ -204,6 +205,9 @@ export function acquireServerBackgroundLifecycle(
204
205
  if (cleanupInProgress) {
205
206
  throw new Error("server background lifecycle cleanup is still in progress");
206
207
  }
208
+ // The quota-pool identity (homeId and the accountRef key) is minted here, per server and off
209
+ // the request path, so GET /api/quota-pool only ever reads it.
210
+ try { ensureQuotaPoolIdentity(); } catch { /* the read reports quota_pool_identity_unavailable */ }
207
211
 
208
212
  const owner: LeaseOwner = {
209
213
  token: Symbol("server-background-lifecycle"),
@@ -81,6 +81,7 @@ import { createNativeChatComboSource, handleNativeChatCompletions, nativeChatDec
81
81
  import { upstreamWireForAdapter, type ProtocolReasonCode } from "../protocols/contract";
82
82
  import { createProtocolEnvelope } from "../protocols/envelope";
83
83
  import { featuresFromChatBody } from "../protocols/features";
84
+ import { credentialDomainFor } from "../protocols/opaque-state";
84
85
  import { checkRepresentable, unrepresentableMessage } from "../protocols/guard";
85
86
  import { requestPathForLane } from "../protocols/path";
86
87
  import { resolveProtocolSettings } from "../protocols/settings";
@@ -344,7 +345,13 @@ async function handleChatCompletionsWithBudget(
344
345
  directConversation = chatConversationToIr(chatBody, chatBody.model as string);
345
346
  internalBody = chatSettingsBody(chatBody);
346
347
  } else {
347
- internalBody = chatToResponsesBody(chatBody);
348
+ internalBody = chatToResponsesBody(chatBody, {
349
+ // #7429: only a route that is and stays first-party Anthropic may receive signed thinking
350
+ // a Chat client returns; a combo, policy or rewritten route resolves its target later.
351
+ acceptAnthropicReasoningSignatures: settledRoute !== null && !routeMayChangeCredentialDomain
352
+ && settledRoute.provider.adapter === "anthropic"
353
+ && credentialDomainFor(settledRoute.provider)?.firstPartyAnthropic === true,
354
+ });
348
355
  }
349
356
  if (effortRow) {
350
357
  internalBody.reasoning = {
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
2
2
  import type { OcxConfig } from "../types";
3
3
  import { canonicalGuiBrowserOrigin, standaloneGuiPairingOrigin } from "../lib/gui-pair-capability";
4
+ import { authorizationCredential } from "../lib/authorization-scheme";
4
5
  import {
5
6
  isAllowedManagementOrigin,
6
7
  isApiAuthRequired,
@@ -409,7 +410,7 @@ function isPairingTransportPermitted(origin: string): boolean {
409
410
 
410
411
  function requestCredential(req: Request): string | null {
411
412
  return req.headers.get("x-opencodex-api-key")?.trim()
412
- || req.headers.get("authorization")?.replace(/^Bearer\s+/i, "").trim()
413
+ || authorizationCredential(req.headers)
413
414
  || null;
414
415
  }
415
416
 
@@ -1,4 +1,5 @@
1
1
  import { createLinkRelaySessions } from "./link-relay-sessions";
2
+ import { createLinkPeerObservations, type LinkPeerEntry } from "../../link/peer-report";
2
3
  import type { Server } from "bun";
3
4
  import {
4
5
  emptyLinkStore,
@@ -9,6 +10,7 @@ import {
9
10
  import { linkStorePath } from "../../link/paths";
10
11
  import { linkRouteAllowed } from "../../link/routes";
11
12
  import { withConfigMutationLockSync } from "../../config/mutation-lock";
13
+ import { registerFallbackLinkResolver } from "../../routing/fallback-policy";
12
14
 
13
15
  export const LINK_INGRESS_HOSTNAME = "opencodex-link.invalid";
14
16
 
@@ -51,6 +53,8 @@ export interface LinkListenerLifecycle<T> {
51
53
  linkAdmissionKeyIds(): ReadonlySet<string>;
52
54
  onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
53
55
  notifyAuthenticatedCatalog(apiKeyId: string): void;
56
+ /** What this Home last learned of a Child over the link; empty until it relays a request. */
57
+ peerObservation(linkId: string): LinkPeerEntry;
54
58
  status(): LinkListenerStatus;
55
59
  close(): Promise<void>;
56
60
  stop(): Promise<void>;
@@ -76,6 +80,7 @@ export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): Lin
76
80
  let stopped = false;
77
81
  const authenticatedCatalogListeners = new Set<(apiKeyId: string) => void>();
78
82
  let lifecycleStatus: LinkListenerStatus = { state: "off", port: null, reason: null };
83
+ const peers = createLinkPeerObservations();
79
84
 
80
85
  const setStatus = (state: LinkListenerStatus["state"], port: number | null, reason: string | null): void => {
81
86
  lifecycleStatus = { state, port, reason };
@@ -89,6 +94,9 @@ export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): Lin
89
94
  const readStoreForAdmission = (): LinkStore => {
90
95
  try { return readStore(storePath); } catch { return emptyLinkStore(); }
91
96
  };
97
+ // OCX-11: the fallback policy resolves a link data key to its link id through this slot.
98
+ const releaseFallbackLinkResolver = registerFallbackLinkResolver(keyId =>
99
+ readStoreForAdmission().links.find(link => link.apiKeyId === keyId)?.id);
92
100
 
93
101
  const bindIfNeeded = (): void => {
94
102
  if (stopped || listener || !startContext) return;
@@ -104,7 +112,7 @@ export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): Lin
104
112
  return;
105
113
  }
106
114
  const requestedPort = store.listenerPort ?? 0;
107
- const sessions = createLinkRelaySessions({ fingerprints: (keyId, linkId) => {
115
+ const sessions = createLinkRelaySessions({ peers, fingerprints: (keyId, linkId) => {
108
116
  if (!readStoreForAdmission().links.some(link => link.id === linkId && link.apiKeyId === keyId)) return [];
109
117
  return startContext?.keyFingerprints?.(keyId) ?? [];
110
118
  } });
@@ -214,12 +222,14 @@ export function createLinkListenerLifecycle<T>(deps: LinkListenerDeps = {}): Lin
214
222
  notifyAuthenticatedCatalog(apiKeyId) {
215
223
  for (const listener of authenticatedCatalogListeners) listener(apiKeyId);
216
224
  },
225
+ peerObservation: linkId => peers.get(linkId),
217
226
  status() {
218
227
  return { ...lifecycleStatus };
219
228
  },
220
229
  close,
221
230
  async stop() {
222
231
  stopped = true;
232
+ releaseFallbackLinkResolver();
223
233
  // Process shutdown is deliberately forceful, unlike last-link removal. Every
224
234
  // authenticated socket is closed, and the client transport cannot reconnect it.
225
235
  relaySessions?.abortReservations();
@@ -14,22 +14,69 @@ import type { ManagementSessionControl } from "../management-auth";
14
14
  import { runPassiveRead } from "../../lib/passive-read";
15
15
  import { LINK_CODEX_SETTINGS_PATH, linkManagementRouteAllowed, linkManagementTarget } from "../../link/management-routes";
16
16
  import { projectHomeCodexSettings } from "../../link/codex-settings";
17
+ import { homeLinkChildParity, LINK_CHILD_PARITY_MAX_BYTES, LINK_CHILD_PARITY_PATH, parseLinkChildParityReport } from "../../link/child-parity";
18
+ import { linkStorePath } from "../../link/paths";
19
+ import { readLinkStore } from "../../link/store";
20
+ import { readBoundedResponseBytes } from "../../lib/bounded-body";
17
21
  import { packageVersion } from "../../lib/package-version";
18
22
 
19
23
  const VERSION = packageVersion("0.0.0");
20
24
  const KEPT_RESPONSE_HEADERS = ["content-type", "content-language", "etag", "last-modified"];
25
+ const REPORT_READ_TIMEOUT_MS = 5_000;
21
26
 
22
27
  function notFound(): Response {
23
28
  return Response.json({ error: "not_found" }, { status: 404, headers: { "cache-control": "no-store" } });
24
29
  }
25
30
 
31
+ function refused(status: number, code: string): Response {
32
+ return Response.json({ error: code }, { status, headers: { "cache-control": "no-store" } });
33
+ }
34
+
35
+ /**
36
+ * `POST` {@link LINK_CHILD_PARITY_PATH}: the Child's apply report (OCX-6). The one write the link
37
+ * accepts, handled here, before and apart from the read-only table, so it never widens what a
38
+ * read may do. It is admitted only for the link the admitting key belongs to, reads at most
39
+ * {@link LINK_CHILD_PARITY_MAX_BYTES} and changes nothing but the in-memory report.
40
+ */
41
+ async function handleChildParityReport(req: Request, url: URL, admittedKeyId: string | undefined): Promise<Response> {
42
+ if (req.method !== "POST" || url.search !== "" || req.headers.has("upgrade")) return notFound();
43
+ if (!admittedKeyId) return refused(403, "link_key_required");
44
+ const declared = Number(req.headers.get("content-length") ?? "0");
45
+ if (Number.isFinite(declared) && declared > LINK_CHILD_PARITY_MAX_BYTES) return refused(413, "report_too_large");
46
+ let bytes: Uint8Array;
47
+ try {
48
+ const read = await readBoundedResponseBytes(new Response(req.body), {
49
+ maxBytes: LINK_CHILD_PARITY_MAX_BYTES,
50
+ signal: AbortSignal.timeout(REPORT_READ_TIMEOUT_MS),
51
+ });
52
+ if (read.oversized) return refused(413, "report_too_large");
53
+ bytes = read.bytes;
54
+ } catch {
55
+ return refused(400, "invalid_report");
56
+ }
57
+ let report: ReturnType<typeof parseLinkChildParityReport>;
58
+ try {
59
+ report = parseLinkChildParityReport(JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)));
60
+ } catch {
61
+ report = null;
62
+ }
63
+ if (!report) return refused(400, "invalid_report");
64
+ let links: ReturnType<typeof readLinkStore>["links"];
65
+ try { links = readLinkStore(linkStorePath()).links; } catch { return refused(503, "link_store_unavailable"); }
66
+ if (!links.some(link => link.id === report.linkId && link.apiKeyId === admittedKeyId)) return refused(403, "link_mismatch");
67
+ homeLinkChildParity.record(report, new Set(links.map(link => link.id)));
68
+ return new Response(null, { status: 204, headers: { "cache-control": "no-store" } });
69
+ }
70
+
26
71
  export async function handleLinkManagementRequest(
27
72
  req: Request,
28
73
  url: URL,
29
74
  config: OcxConfig,
30
75
  deps: ManagementApiDeps,
31
76
  sessionControl?: ManagementSessionControl,
77
+ admittedKeyId?: string,
32
78
  ): Promise<Response> {
79
+ if (url.pathname === LINK_CHILD_PARITY_PATH) return handleChildParityReport(req, url, admittedKeyId);
33
80
  if (!linkManagementRouteAllowed(url, req)) return notFound();
34
81
  if (url.pathname === LINK_CODEX_SETTINGS_PATH) {
35
82
  return Response.json(projectHomeCodexSettings(config, VERSION), { headers: { "cache-control": "no-store" } });
@@ -3,6 +3,7 @@ import {
3
3
  LINK_RELAY_AUTH_PATH, LINK_RELAY_AUTH_TIMEOUT_MS, LINK_RELAY_AUTH_VERSION,
4
4
  LINK_RELAY_SESSION_HEADER, linkRelayProof, linkRelayProofMatches,
5
5
  } from "../../link/relay-auth";
6
+ import { parseLinkPeerReport, type LinkPeerObservations } from "../../link/peer-report";
6
7
 
7
8
  interface Session {
8
9
  peer: string;
@@ -18,6 +19,8 @@ export interface LinkRelaySessionOptions {
18
19
  fingerprints: (keyId: string, linkId: string) => readonly string[];
19
20
  /** Tests can use a bounded shorter reservation; production uses five seconds. */
20
21
  reservationMs?: number;
22
+ /** Where a verified challenge records the Child's report, and a known link's version mismatch. */
23
+ peers?: LinkPeerObservations;
21
24
  }
22
25
 
23
26
  /** One book per bound listener. A pending proof reserves the bind before the data request arrives. */
@@ -64,14 +67,20 @@ export function createLinkRelaySessions(options: LinkRelaySessionOptions) {
64
67
  if (closing || req.method !== "GET" || sessions.size >= 256 || req.headers.has("upgrade")
65
68
  || req.headers.has("authorization") || req.headers.has("x-opencodex-api-key")) return refuse();
66
69
  const q = url.searchParams;
67
- if (q.get("version") !== LINK_RELAY_AUTH_VERSION || [...q.keys()].length !== 5
68
- || ["version", "key", "link", "nonce", "proof"].some(name => q.getAll(name).length !== 1)) return refuse();
70
+ if ([...q.keys()].length !== 5 || ["version", "key", "link", "nonce", "proof"].some(name => q.getAll(name).length !== 1)) return refuse();
69
71
  const keyId = q.get("key")!, linkId = q.get("link")!, nonce = q.get("nonce")!;
72
+ if (q.get("version") !== LINK_RELAY_AUTH_VERSION) {
73
+ // Unverifiable, but only for a link and key this Home issued: status names the mismatch.
74
+ const version = q.get("version")!;
75
+ if (/^[1-9]\d{0,3}$/.test(version) && options.fingerprints(keyId, linkId).length > 0) options.peers?.refuse(linkId, version);
76
+ return refuse();
77
+ }
70
78
  const peer = peerOf(req, server as Server<unknown>);
71
79
  if (!peer || sessions.has(nonce)) return refuse();
72
80
  const fingerprint = options.fingerprints(keyId, linkId).find(candidate =>
73
81
  linkRelayProofMatches(q.get("proof"), linkRelayProof(candidate, "caller", keyId, linkId, nonce)));
74
82
  if (!fingerprint) return refuse();
83
+ options.peers?.observe(linkId, LINK_RELAY_AUTH_VERSION, parseLinkPeerReport(req.headers));
75
84
  const release = acquire();
76
85
  const timer = setTimeout(() => { consume(nonce)?.release(); }, reservationMs);
77
86
  sessions.set(nonce, { peer, keyId, linkId, fingerprint, release, timer, expiresAt: Date.now() + reservationMs });
@@ -15,6 +15,7 @@ import {
15
15
  } from "./link-listener";
16
16
  import { createLinkSupervisor, type LinkSupervisor } from "../../link/supervisor";
17
17
  import { linkManagementRouteAllowed } from "../../link/management-routes";
18
+ import { linkChildParityRouteAllowed } from "../../link/child-parity";
18
19
  import { saveConfigPreservingClaudeCode } from "../../config/live-reconcile";
19
20
  import { reconcileLiveStateStores } from "../../lib/state-store-registrations";
20
21
  import { startJevQuotaActivation } from "../../combos/jev-quota-activation";
@@ -38,6 +39,7 @@ export interface OptionalListenerSet<T> {
38
39
  linkAdmissionKeyIds(): ReadonlySet<string>;
39
40
  onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
40
41
  notifyAuthenticatedCatalog(apiKeyId: string): void;
42
+ peerObservation(linkId: string): import("../../link/peer-report").LinkPeerEntry;
41
43
  status(): LinkListenerStatus;
42
44
  linkStatus(): LinkListenerStatus;
43
45
  linkSupervisor(): LinkSupervisor;
@@ -83,11 +85,14 @@ export function createOptionalListenerSet<T>(linkDeps: LinkListenerDeps = {}): O
83
85
  },
84
86
  // The Home's link listener admits the data plane plus the read-only management relay
85
87
  // (#6957). The Child's own ingress keeps the data-only table, so a local caller on the Child
86
- // cannot reach the Home's management reads without the Child's dashboard session.
87
- linkRouteAllowed: (url, req) => linkRouteAllowed(url, req) || linkManagementRouteAllowed(url, req),
88
+ // cannot reach the Home's management reads without the Child's dashboard session. The one
89
+ // write is the Child's apply report (OCX-6), which changes only an in-memory status field.
90
+ linkRouteAllowed: (url, req) => linkRouteAllowed(url, req) || linkManagementRouteAllowed(url, req)
91
+ || linkChildParityRouteAllowed(url, req),
88
92
  linkAdmissionKeyIds: () => linkListener.linkAdmissionKeyIds(),
89
93
  onAuthenticatedCatalog: listener => linkListener.onAuthenticatedCatalog(listener),
90
94
  notifyAuthenticatedCatalog: apiKeyId => linkListener.notifyAuthenticatedCatalog(apiKeyId),
95
+ peerObservation: linkId => linkListener.peerObservation(linkId),
91
96
  status: () => linkListener.status(),
92
97
  linkStatus: () => linkListener.status(),
93
98
  linkSupervisor: () => supervisor,
@@ -204,11 +204,13 @@ import type { ReadinessGate } from "../readiness";
204
204
  import type { WorkflowRefusalLog } from "../workflow-refusal";
205
205
 
206
206
  import { readyProtocolMetadata } from "../../remote/protocol";
207
+ import { homeInstanceId, stampServedBy } from "../../remote/served-by";
207
208
  import { modelCapabilityFields } from "../models-capabilities";
208
209
  import { createWebsocketHandler } from "./websocket-handler";
209
210
  import { reasoningArchiveErrorResponse } from "../responses/reasoning-archive-error";
210
211
  import { withGrokSessionIdentity } from "../../grok/session-identity";
211
212
  import { withCallerSessionIdentity } from "../caller-session-identity";
213
+ import { agentAssertionUnsupportedResponse, hasAgentAssertionAuthorization } from "../../lib/authorization-scheme";
212
214
 
213
215
  export type ServerIngress = "public" | "unauthenticated-loopback" | "hub-management" | "claude-intercept" | "hub-link";
214
216
 
@@ -370,6 +372,10 @@ export function createServeOptions(ctx: ServeOptionsContext) {
370
372
  : ingress === "unauthenticated-loopback" || ingress === "claude-intercept"
371
373
  ? loopbackPolicy()
372
374
  : config;
375
+ // Codex runtime agent assertion_only (Authorization: AgentAssertion) is refused before
376
+ // admission or routing: it is never a bearer, never an admission secret, and no pool or
377
+ // routed credential may stand in for it.
378
+ if (hasAgentAssertionAuthorization(req.headers)) return withCors(agentAssertionUnsupportedResponse(), req, policy);
373
379
  if (ingress === "hub-link") {
374
380
  const admission = resolveApiAuth(req, policy);
375
381
  if (!admission) return withCors(formatErrorResponse(401, "authentication_error", "opencodex API key required"), req, policy);
@@ -380,7 +386,10 @@ export function createServeOptions(ctx: ServeOptionsContext) {
380
386
  // the shared table in src/link/management-routes.ts; never reachable on another ingress.
381
387
  if (isLinkManagementPath(requestUrl.pathname)) {
382
388
  const { handleLinkManagementRequest } = await import("./link-management");
383
- return withCors(await handleLinkManagementRequest(req, requestUrl, config, requestManagementApiDeps, managementSessionControl), req, policy);
389
+ return withCors(await handleLinkManagementRequest(
390
+ req, requestUrl, config, requestManagementApiDeps, managementSessionControl,
391
+ admission.kind === "configured" ? admission.keyId : undefined,
392
+ ), req, policy);
384
393
  }
385
394
  }
386
395
  const url = requestUrl;
@@ -408,11 +417,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
408
417
  const message = "OpenCodex package files changed while this proxy was running; it restarts on its own, or run 'ocx restart' ('ocx service restart' for a background service).";
409
418
  const fencedPort = ctx.boundPort ?? requestServer.port ?? listenPort;
410
419
  const fencedHealth = url.pathname === "/healthz";
420
+ const fencedInstanceId = fencedHealth ? homeInstanceId() : undefined;
411
421
  const response = fencedHealth || readyzPath !== undefined
412
422
  ? jsonResponse({
413
423
  status: "restart_required",
414
424
  service: "opencodex",
415
425
  version: VERSION,
426
+ ...(fencedInstanceId ? { instanceId: fencedInstanceId } : {}),
416
427
  uptime: process.uptime(),
417
428
  pid: process.pid,
418
429
  port: fencedPort,
@@ -623,10 +634,13 @@ export function createServeOptions(ctx: ServeOptionsContext) {
623
634
  if (url.pathname === "/healthz" && req.method === "GET") {
624
635
  // service/pid/port let CLI liveness reject foreign 200s and verify pid identity.
625
636
  const healthPort = ctx.server.port ?? listenPort;
637
+ // Opaque Home instance id (src/remote/served-by.ts); omitted until the identity exists.
638
+ const instanceId = homeInstanceId();
626
639
  const response = jsonResponse({
627
640
  status: "ok",
628
641
  service: "opencodex",
629
642
  version: VERSION,
643
+ ...(instanceId ? { instanceId } : {}),
630
644
  uptime: process.uptime(),
631
645
  pid: process.pid,
632
646
  port: healthPort,
@@ -1977,5 +1991,8 @@ export function createServeOptions(ctx: ServeOptionsContext) {
1977
1991
  },
1978
1992
  websocket: createWebsocketHandler(ctx, requestMetrics),
1979
1993
  } as const;
1980
- return serveOptions;
1994
+ const routeFetch = serveOptions.fetch;
1995
+ // x-opencodex-served-by on GET /v1/models, whichever branch answered (src/remote/served-by.ts).
1996
+ return { ...serveOptions, fetch: async (req: Request, requestServer: Server<WsData>): Promise<Response> =>
1997
+ stampServedBy(req.method, codexCompatibleUrl(req.url).pathname, await routeFetch(req, requestServer)) } as const;
1981
1998
  }
@@ -88,6 +88,10 @@ export const LIVE_SIDEBAND_API_ROOT = "https://api.openai.com/v1";
88
88
  * upstream the caller was addressing, so there is nothing to scope it away from. That is not
89
89
  * true of the images sidecar, which strips it deliberately and keeps doing so.
90
90
  *
91
+ * `voice-session-id` is the client-generated voice session id Codex 0.164 forwards on WebSocket,
92
+ * WebRTC and existing-call transports (openai/codex#53784). It is a separate identity from the
93
+ * realtime session id carried in `x-session-id`, so neither may stand in for the other.
94
+ *
91
95
  * Every name here is relayed only when the caller sent it. Nothing on this list is invented,
92
96
  * which is what keeps a caller that omits one byte-identical upstream. Auth headers
93
97
  * (`authorization`, `chatgpt-account-id`) stay proxy-owned and are never taken from this list.
@@ -100,6 +104,7 @@ export const LIVE_CLIENT_PROTOCOL_HEADERS = [
100
104
  "originator",
101
105
  "x-oai-attestation",
102
106
  "x-codex-turn-metadata",
107
+ "voice-session-id",
103
108
  ] as const;
104
109
 
105
110
  /**
@@ -156,7 +156,7 @@ export interface ManagementApiDeps {
156
156
  codexPromptPaths?: CodexPromptPaths;
157
157
  /** Link seams are getters so the optional listener and supervisor are singletons. */
158
158
  linkSupervisor?: () => LinkSupervisor;
159
- linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog"> & Partial<Pick<import("../index/optional-listeners").OptionalListenerSet<unknown>, "ensureClaudeIntercept" | "claudeInterceptOutcome">>;
159
+ linkListener?: () => Pick<LinkListenerLifecycle<unknown>, "ensureStarted" | "status" | "close" | "onAuthenticatedCatalog"> & Partial<Pick<LinkListenerLifecycle<unknown>, "peerObservation">> & Partial<Pick<import("../index/optional-listeners").OptionalListenerSet<unknown>, "ensureClaudeIntercept" | "claudeInterceptOutcome">>;
160
160
  readLinkStore?: () => LinkStore;
161
161
  writeLinkStore?: (store: LinkStore) => void;
162
162
  linkKnownHostsPath?: () => string;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * `GET|PUT /api/fallback`: the OCX-11 fallback state (`ocx.fallback/1`) and its one scoped write.
3
+ *
4
+ * GET is a passive read. A Remote Link Child reaches it through the read-only link relay
5
+ * (`LINK_MANAGEMENT_READ_PATHS`); there it drops link aliases (`buildFallbackState` passive). PUT
6
+ * runs on the Home only: a Child's relay and CLI refuse writes before sending.
7
+ */
8
+ import type { ManagementContext } from "./context";
9
+ import { jsonResponse } from "../auth-cors";
10
+ import { mutatePersistedConfig } from "../../config";
11
+ import { isPassiveRead } from "../../lib/passive-read";
12
+ import { readLinkStore, type LinkRecord } from "../../link/store";
13
+ import { linkStorePath } from "../../link/paths";
14
+ import { applyFallbackWrite, buildFallbackState, parseFallbackWrites } from "../../routing/fallback-state";
15
+ import { managementBodyTooLargeResponse, readManagementJsonBody } from "./body";
16
+
17
+ function links(ctx: ManagementContext): LinkRecord[] {
18
+ try {
19
+ return (ctx.deps.readLinkStore?.() ?? readLinkStore(linkStorePath())).links;
20
+ } catch {
21
+ return [];
22
+ }
23
+ }
24
+
25
+ function view(ctx: ManagementContext): Response {
26
+ const response = jsonResponse(buildFallbackState(ctx.config, { links: links(ctx), passive: isPassiveRead() }), 200, ctx.req, ctx.config);
27
+ response.headers.set("cache-control", "no-store");
28
+ return response;
29
+ }
30
+
31
+ export async function handleFallbackRoutes(ctx: ManagementContext): Promise<Response | null> {
32
+ const { req, url, config } = ctx;
33
+ if (url.pathname === "/api/fallback" && req.method === "GET") return view(ctx);
34
+ if (url.pathname === "/api/fallback" && req.method === "PUT") return await writeFallback(ctx);
35
+ return null;
36
+ }
37
+
38
+ async function writeFallback(ctx: ManagementContext): Promise<Response> {
39
+ const { req, config } = ctx;
40
+ let body: unknown;
41
+ try {
42
+ body = await readManagementJsonBody(req);
43
+ } catch (error) {
44
+ return managementBodyTooLargeResponse(error, req, config)
45
+ ?? jsonResponse({ error: "invalid JSON body", code: "invalid_request" }, 400, req, config);
46
+ }
47
+ const parsed = parseFallbackWrites(body);
48
+ if (!parsed.ok) return jsonResponse({ error: parsed.error, code: "invalid_request" }, 400, req, config);
49
+ const known = links(ctx);
50
+ for (const write of parsed.writes) {
51
+ if (write.scope !== "device") continue;
52
+ // A link alias names its link; otherwise the key must be a known link id or data key id.
53
+ const byAlias = known.find(link => link.alias === write.key);
54
+ if (byAlias) write.key = byAlias.id;
55
+ // Clearing stays possible for a device that is gone (a revoked link, a deleted data key):
56
+ // its entry still lists in the state, and clearing a missing entry is a no-op.
57
+ if (write.value === null) continue;
58
+ const exists = known.some(link => link.id === write.key)
59
+ || (config.apiKeys ?? []).some(entry => entry.id === write.key);
60
+ if (!exists) {
61
+ return jsonResponse({ error: "unknown device: use a link id or alias from ocx link status, or a Hub data key id", code: "unknown_device" }, 404, req, config);
62
+ }
63
+ }
64
+ const outcome = mutatePersistedConfig(persisted => {
65
+ let changed = false;
66
+ for (const write of parsed.writes) changed = applyFallbackWrite(persisted, write) || changed;
67
+ return { changed, value: persisted.fallback === undefined ? undefined : structuredClone(persisted.fallback) };
68
+ });
69
+ if (outcome.status === "unavailable") {
70
+ return jsonResponse({ error: "config.json is " + outcome.reason + "; fallback settings were not saved", code: "config_unavailable" }, 409, req, config);
71
+ }
72
+ if (outcome.value === undefined) delete config.fallback;
73
+ else config.fallback = outcome.value;
74
+ return view(ctx);
75
+ }
@@ -3,6 +3,8 @@ import { tmpdir } from "node:os";
3
3
  import { join } from "node:path";
4
4
  import { assertSshAlias, buildExecArgv, buildFingerprintArgv, buildProbeArgv, REMOTE_COMMAND_NOT_FOUND, REMOTE_PLATFORM_UNSUPPORTED, remoteOcxArgv } from "../../link/ssh-argv";
5
5
  import { parseFingerprintLine } from "../../link/fingerprint";
6
+ import { detectRemotePlatform, isWindowsShellRefusal, WINDOWS_CHILD_REFUSAL } from "../../link/remote-platform";
7
+ import type { SshRunner } from "../../link/ssh-runner";
6
8
  import { LINK_CATALOG_MAX_BYTES, LINK_RELAY_VERIFY_MS, isChildHealth, linkCommandBudget, linkRestartBudget, parseChildHttp, remoteChildHttpArgv, REMOTE_CURL_PREFLIGHT } from "../../link/remote-probe";
7
9
  import { newLinkId, type LinkStore } from "../../link/store";
8
10
  import { SshRunnerError, boundHint, sshFailureHint, sshRunnerErrorHint, type SshRunResult } from "../../link/ssh-runner";
@@ -24,11 +26,23 @@ async function serialized(state: LinkRouteState, alias: string, action: () => Pr
24
26
  }
25
27
 
26
28
  function remoteFailure(result: SshRunResult, code: string, message: string): Response | null {
27
- if (result.code === REMOTE_PLATFORM_UNSUPPORTED) return fail("remote_platform_unsupported", "Remote Link Children require macOS or Linux; Windows is unsupported.", 409);
29
+ if (result.code === REMOTE_PLATFORM_UNSUPPORTED || (result.code !== 0 && isWindowsShellRefusal(result.stderr))) return windowsRefusal();
28
30
  if (result.code === REMOTE_COMMAND_NOT_FOUND) return fail("remote_ocx_missing", "ocx was not found on the remote host.", 502, sshFailureHint(result.stderr));
29
31
  return result.code === 0 ? null : fail(code, message, 502, sshFailureHint(result.stderr));
30
32
  }
31
33
 
34
+ function windowsRefusal(): Response {
35
+ return fail("remote_platform_unsupported", WINDOWS_CHILD_REFUSAL, 409);
36
+ }
37
+
38
+ /**
39
+ * Pre-issuance Windows gate for a failed remote command: exit 125, the English cmd/PowerShell
40
+ * refusal, or (for localized shells) the fixed %OS% probe against the same host key.
41
+ */
42
+ async function windowsChild(runner: SshRunner, alias: string, knownHostsFile: string, result: SshRunResult): Promise<boolean> {
43
+ return result.code !== 0 && await detectRemotePlatform(runner, alias, knownHostsFile, result) === "windows";
44
+ }
45
+
32
46
  export async function probe(ctx: ManagementContext, state: LinkRouteState): Promise<Response> {
33
47
  const value = await readManagementJsonBodyOr(ctx.req, null);
34
48
  const body = exactBody(value, ["alias"]) ?? exactBody(value, ["alias", "includeVersion"]);
@@ -44,7 +58,8 @@ export async function probe(ctx: ManagementContext, state: LinkRouteState): Prom
44
58
  const started = now();
45
59
  const result = await runner.run(buildProbeArgv({ alias, tempKnownHostsFile: path }), { timeoutMs: 30_000 });
46
60
  const latency = Math.max(0, now() - started);
47
- if (result.code !== 0) return fail("probe_failed", "SSH host probing failed.", 502, sshFailureHint(result.stderr));
61
+ // cmd.exe and PowerShell refuse the POSIX `true` the probe runs; say Windows, not "probe failed".
62
+ if (result.code !== 0) return await windowsChild(runner, alias, path, result) ? windowsRefusal() : fail("probe_failed", "SSH host probing failed.", 502, sshFailureHint(result.stderr));
48
63
  const fp = await runner.run(buildFingerprintArgv(path), { timeoutMs: 10_000 });
49
64
  if (fp.code !== 0) return fail("fingerprint_failed", "The SSH host fingerprint could not be read.", 502, sshFailureHint(fp.stderr));
50
65
  const parsed = parseFingerprintLine(fp.stdout.trim());
@@ -53,6 +68,7 @@ export async function probe(ctx: ManagementContext, state: LinkRouteState): Prom
53
68
  let ocxVersion: string | undefined;
54
69
  if (body.includeVersion === true) {
55
70
  const versionResult = await runner.run(buildExecArgv({ alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
71
+ if (await windowsChild(runner, alias, path, versionResult)) return windowsRefusal();
56
72
  const denied = remoteFailure(versionResult, "version_probe_failed", "The remote ocx version could not be read.");
57
73
  if (denied) return denied;
58
74
  const parsedVersion = parseVersion(versionResult.stdout);
@@ -84,7 +100,9 @@ export async function confirmHost(ctx: ManagementContext, state: LinkRouteState)
84
100
  writeFileSync(path, pending.knownHostLine + "\n", { mode: 0o600 });
85
101
  try {
86
102
  const started = now();
87
- const result = await runnerFor(ctx).run(buildExecArgv({ alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
103
+ const runner = runnerFor(ctx);
104
+ const result = await runner.run(buildExecArgv({ alias, argv: remoteOcxArgv(["--version"]), knownHostsFile: path }), { timeoutMs: 30_000 });
105
+ if (await windowsChild(runner, alias, path, result)) return windowsRefusal();
88
106
  const denied = remoteFailure(result, "version_probe_failed", "The remote ocx version could not be confirmed.");
89
107
  if (denied) return denied;
90
108
  const version = parseVersion(result.stdout);
@@ -27,6 +27,8 @@ export interface LinkRouteListener {
27
27
  status(): { state: "off" | "listening" | "failed"; port: number | null; reason: string | null };
28
28
  close(): Promise<void>;
29
29
  onAuthenticatedCatalog(listener: (apiKeyId: string) => void): () => void;
30
+ /** Optional so route fakes stay small; without it every link reports an unknown peer. */
31
+ peerObservation?(linkId: string): import("../../link/peer-report").LinkPeerEntry;
30
32
  }
31
33
 
32
34
  export interface LinkRouteState {
@@ -6,7 +6,11 @@ import { clearCompensationFailed, compensationPath, readCompensation } from "../
6
6
  import { loadHostCandidates } from "../../link/ssh-config";
7
7
  import { newLinkId, type LinkStore } from "../../link/store";
8
8
  import { sshFailureHint, sshRunnerErrorHint } from "../../link/ssh-runner";
9
- import { projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
9
+ import { attachLinkPeers, projectLinkStatus, type LinkStatusDto } from "../../link/status-projection";
10
+ import { homeLinkChildParity } from "../../link/child-parity";
11
+ import { localLinkPeer } from "../../link/compatibility";
12
+ import { packageVersion } from "../../lib/package-version";
13
+ import { buildPeerFeatures } from "../../remote/features";
10
14
  import { isLinkPort } from "../../link/ports";
11
15
  import { joinHome, type ClientLinkJoinDeps } from "../../client/link-join";
12
16
  import { clientLinkStatusIdentity } from "../../client/link-status";
@@ -268,6 +272,7 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
268
272
  store, state.supervisor.status(), listenerStatus, ctx.config, readCompensation(),
269
273
  ctx.config.runtimeRole === "client" ? clientLinkTunnelStatus() : null,
270
274
  ctx.config.runtimeRole === "client" && ctx.config.client?.link ? clientLinkStatusIdentity(ctx.config.client.link.linkId) : null,
275
+ linkId => homeLinkChildParity.get(linkId),
271
276
  );
272
277
  for (const link of dto.links) {
273
278
  const failure = state.compensationFailures?.get(link.id);
@@ -277,6 +282,12 @@ export async function handleLinkRoutes(ctx: ManagementContext, suppliedState?: L
277
282
  const failure = state.compensationFailures?.get(store.links[0].id);
278
283
  if (failure) Object.assign(dto.child, { state: "failed" as const, since: failure.since, reason: failure.reason });
279
284
  }
285
+ // Opt-in: an older `ocx link status` validates the document key by key and would reject the
286
+ // added fields, so only a caller that asks for compatibility gets `local` and `peer`.
287
+ if (url.searchParams.get("compatibility") === "1") {
288
+ attachLinkPeers(dto, localLinkPeer(packageVersion("0.0.0"), buildPeerFeatures()),
289
+ linkId => state.listener.peerObservation?.(linkId) ?? { observed: null, refused: null });
290
+ }
280
291
  // A dashboard session also learns why it cannot join as a Child. The admin-token answer stays
281
292
  // the exact K16 document that `ocx link status` validates key by key.
282
293
  const denial = ctx.principal === "gui-session" ? joinDenied(ctx) : null;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * `GET /api/quota-pool`: the `ocx.quota-pool/1` read GOAT consumes (see src/quota-pool/).
3
+ *
4
+ * Same management authorization as the neighbouring reads; a Remote Link Child reaches it through
5
+ * the read-only link relay (`LINK_MANAGEMENT_READ_PATHS`) and a Hub client through its hub relay.
6
+ * The read is always passive: there is no refresh form, so nothing here can make the Home probe.
7
+ */
8
+ import type { ManagementContext } from "./context";
9
+ import { jsonResponse } from "../auth-cors";
10
+
11
+ export async function handleQuotaPoolRoutes(ctx: ManagementContext): Promise<Response | null> {
12
+ const { req, url, config } = ctx;
13
+ // Any query (including a stray refresh=1) is ignored: the read has no active form.
14
+ if (url.pathname === "/api/quota-pool" && req.method === "GET") {
15
+ const { readQuotaPool } = await import("../../quota-pool/assemble");
16
+ const result = await readQuotaPool(config);
17
+ const response = result.ok
18
+ ? jsonResponse(result.pool, 200, req, config)
19
+ : jsonResponse({ error: result.error }, 503, req, config);
20
+ response.headers.set("cache-control", "no-store");
21
+ return response;
22
+ }
23
+ return null;
24
+ }
@@ -370,6 +370,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
370
370
  { method: "GET", path: "/api/provider-context-caps", module: "server/management/provider-routes", mutates: false },
371
371
  { method: "GET", path: "/api/provider-presets", module: "server/management/provider-routes", mutates: false },
372
372
  { method: "GET", path: "/api/provider-quotas", module: "server/management/provider-routes", mutates: false },
373
+ { method: "GET", path: "/api/quota-pool", module: "server/management/quota-pool-routes", mutates: false, exempt: { reason: "deferred-verb", why: "The ocx.quota-pool/1 read is a machine contract GOAT consumes over management and the link relay; the CLI shows the same caches through ocx provider quota and ocx account.", owner: "ocx-1 quota pool", ownerDoc: "structure/quota-pool.md" } },
374
+ { method: "GET", path: "/api/fallback", module: "server/management/fallback-routes", mutates: false },
375
+ { method: "PUT", path: "/api/fallback", module: "server/management/fallback-routes", mutates: true },
373
376
  { method: "GET", path: "/api/provider-request-pacing", module: "server/management/provider-key-pacing-routes", mutates: false },
374
377
  { method: "GET", path: "/api/providers", module: "server/management/provider-routes", mutates: false },
375
378
  { method: "PATCH", path: "/api/providers", module: "server/management/provider-routes", mutates: true },
@@ -161,6 +161,20 @@ async function handleLowQuotaRoutesOnDemand(ctx: ManagementContext): Promise<Res
161
161
  return handleLowQuotaRoutes(ctx);
162
162
  }
163
163
 
164
+ /** Lazy for the same reason: the quota-pool assembler pulls every quota cache reader. */
165
+ async function handleQuotaPoolRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
166
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/quota-pool", false)) return null;
167
+ const { handleQuotaPoolRoutes } = await import("./management/quota-pool-routes");
168
+ return handleQuotaPoolRoutes(ctx);
169
+ }
170
+
171
+ /** Lazy: the OCX-11 fallback state view and its write live off the dashboard's common path. */
172
+ async function handleFallbackRoutesOnDemand(ctx: ManagementContext): Promise<Response | null> {
173
+ if (!pathInManagementNamespace(ctx.url.pathname, "/api/fallback", false)) return null;
174
+ const { handleFallbackRoutes } = await import("./management/fallback-routes");
175
+ return handleFallbackRoutes(ctx);
176
+ }
177
+
164
178
  /**
165
179
  * Lazy like the Lab and routing-profile handlers, and for the same recorded reason: this file is
166
180
  * mounted for every dashboard request, so a static import would put the workflow-budget ledger
@@ -351,6 +365,8 @@ export async function handleManagementAPI(
351
365
  ?? (await handleRequestHistoryRoutes(ctx))
352
366
  ?? (await handleQuotaResetRoutesOnDemand(ctx))
353
367
  ?? (await handleLowQuotaRoutesOnDemand(ctx))
368
+ ?? (await handleQuotaPoolRoutesOnDemand(ctx))
369
+ ?? (await handleFallbackRoutesOnDemand(ctx))
354
370
  ?? (await handleWorkflowBudgetRoutesOnDemand(ctx))
355
371
  ?? (await handleCodexAgentRoleRoutesOnDemand(ctx))
356
372
  ?? (await handleProtocolRoutesOnDemand(ctx))