@bitkyc08/opencodex 2.91.0 → 2.92.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 (148) hide show
  1. package/bin/ocx.mjs +37 -3
  2. package/gui/dist/assets/{App-nZ34cR5G.js → App-sUf_8afJ.js} +18 -18
  3. package/gui/dist/assets/{Tray-BuLy7hL1.js → Tray-BSsxznDm.js} +1 -1
  4. package/gui/dist/assets/index-BlFE_VB_.js +94 -0
  5. package/gui/dist/assets/quota-summary-BRlbXgw0.js +1 -0
  6. package/gui/dist/index.html +1 -1
  7. package/gui/dist/provider-icons/workbuddy-color.svg +29 -0
  8. package/package.json +4 -3
  9. package/src/adapters/google-vertex-endpoint.ts +9 -1
  10. package/src/adapters/google-vertex-key-project.ts +223 -0
  11. package/src/adapters/google.ts +13 -3
  12. package/src/adapters/openai-chat/tool-name-registry.ts +3 -1
  13. package/src/adapters/openai-chat.ts +2 -2
  14. package/src/adapters/openai-responses/request-strips.ts +60 -0
  15. package/src/adapters/registry.ts +5 -0
  16. package/src/adapters/workbuddy-schema.ts +61 -0
  17. package/src/adapters/workbuddy.ts +68 -0
  18. package/src/bridge/response-json.ts +1 -1
  19. package/src/bridge/sse.ts +1 -1
  20. package/src/chat/outbound.ts +11 -4
  21. package/src/cli/capabilities-agents-routing.ts +3 -3
  22. package/src/cli/codex-cli-update.ts +8 -0
  23. package/src/cli/combo-result.ts +6 -0
  24. package/src/cli/combo-stats.ts +3 -0
  25. package/src/cli/combo.ts +66 -42
  26. package/src/cli/launcher-context.ts +49 -2
  27. package/src/cli/link.ts +45 -32
  28. package/src/cli/provider.ts +2 -1
  29. package/src/cli/usage-compare.ts +68 -12
  30. package/src/client/link-ingress.ts +1 -1
  31. package/src/client/link-readiness.ts +97 -0
  32. package/src/client/link-relay-transport.ts +79 -11
  33. package/src/client/link-relay.ts +74 -15
  34. package/src/client/link-tunnel.ts +45 -18
  35. package/src/client/machine-listener.ts +20 -2
  36. package/src/client/runtime.ts +20 -2
  37. package/src/codex/account-store.ts +17 -3
  38. package/src/codex/app-server-restart-service.ts +6 -5
  39. package/src/codex/catalog/effort.ts +9 -2
  40. package/src/codex/catalog/gather-capture.ts +4 -2
  41. package/src/codex/catalog/provider-models.ts +14 -8
  42. package/src/codex/chatgpt-refresh-failure.ts +6 -2
  43. package/src/codex/cli-install-provenance.ts +418 -0
  44. package/src/codex/cli-installation-targets.ts +5 -1
  45. package/src/codex/cli-update-plan.ts +43 -16
  46. package/src/codex/desktop-app/picker-freshness.ts +10 -8
  47. package/src/codex/desktop-compatibility/usage-activation.ts +2 -1
  48. package/src/codex/desktop-compatibility/usage-controller.ts +7 -1
  49. package/src/codex/pool-refresh-backoff.ts +3 -1
  50. package/src/codex/user-identity.ts +35 -6
  51. package/src/codex/windows-current-sid.ts +162 -0
  52. package/src/combos/jev-quota-activation.ts +5 -1
  53. package/src/combos/jev-quota-route.ts +1 -1
  54. package/src/combos/jev-quota.ts +9 -1
  55. package/src/combos/jev-session-pin-config.ts +51 -0
  56. package/src/combos/jev.ts +6 -0
  57. package/src/combos/types.ts +19 -0
  58. package/src/config/provider-validation.ts +7 -0
  59. package/src/config/schema/leaf-validators.ts +1 -0
  60. package/src/generated/compatibility-version.json +194 -122
  61. package/src/lib/decision-quota-sink.ts +10 -0
  62. package/src/lib/translator-budget.ts +4 -4
  63. package/src/lib/upstream-retry.ts +4 -1
  64. package/src/link/diagnostics.ts +193 -0
  65. package/src/link/relay-auth.ts +6 -0
  66. package/src/link/status-projection.ts +39 -0
  67. package/src/link/supervisor.ts +45 -12
  68. package/src/link/tunnel-history.ts +109 -0
  69. package/src/link/tunnel-state.ts +80 -17
  70. package/src/messaging/remote-files.ts +24 -1
  71. package/src/oauth/generic-account-failover.ts +91 -7
  72. package/src/oauth/index.ts +15 -0
  73. package/src/oauth/model-discovery-auth.ts +5 -0
  74. package/src/oauth/store.ts +4 -1
  75. package/src/oauth/token-guardian.ts +7 -0
  76. package/src/oauth/types.ts +9 -0
  77. package/src/oauth/workbuddy.ts +139 -0
  78. package/src/providers/antigravity-quota-identity.ts +37 -0
  79. package/src/providers/base-url-choices.ts +8 -5
  80. package/src/providers/derive.ts +23 -3
  81. package/src/providers/model-discovery.ts +2 -0
  82. package/src/providers/model-rename-fields.ts +1 -0
  83. package/src/providers/quota/account-cache.ts +3 -2
  84. package/src/providers/quota/antigravity.ts +25 -14
  85. package/src/providers/quota/report-cache.ts +13 -4
  86. package/src/providers/quota/vendor-probes-key.ts +60 -0
  87. package/src/providers/quota/workbuddy.ts +83 -0
  88. package/src/providers/quota-decision-antigravity.ts +64 -0
  89. package/src/providers/quota-decision-publication.ts +11 -0
  90. package/src/providers/quota-decision-snapshot.ts +21 -4
  91. package/src/providers/quota-types.ts +2 -0
  92. package/src/providers/quota.ts +3 -1
  93. package/src/providers/registry/entries-core.ts +9 -0
  94. package/src/providers/registry/entries-extended.ts +20 -7
  95. package/src/providers/registry/model-ids.ts +3 -0
  96. package/src/providers/registry/model-seeds.ts +27 -28
  97. package/src/providers/registry/types.ts +19 -1
  98. package/src/providers/resolved-model-policy.ts +4 -4
  99. package/src/providers/routed-config.ts +15 -0
  100. package/src/providers/stale-model-roster-migration.ts +83 -15
  101. package/src/providers/workbuddy-credit-balance.ts +81 -0
  102. package/src/providers/workbuddy-models.ts +37 -0
  103. package/src/providers/workbuddy.ts +60 -0
  104. package/src/responses/reasoning-replay-cache.ts +18 -14
  105. package/src/router.ts +2 -0
  106. package/src/routing/analytics.ts +2 -3
  107. package/src/routing/fallback-policy.ts +3 -3
  108. package/src/server/auth-cors.ts +1 -0
  109. package/src/server/chat-completions.ts +14 -1
  110. package/src/server/chat-native-reasoning.ts +10 -3
  111. package/src/server/chat-native.ts +13 -2
  112. package/src/server/claude-messages.ts +3 -0
  113. package/src/server/index/link-relay-sessions.ts +9 -5
  114. package/src/server/index/optional-listeners.ts +4 -0
  115. package/src/server/local-management-read-client.ts +3 -1
  116. package/src/server/management/combo-routes.ts +10 -4
  117. package/src/server/management/link-routes.ts +5 -2
  118. package/src/server/management/provider-routes.ts +1 -1
  119. package/src/server/port-reclaim.ts +51 -9
  120. package/src/server/request-log-conversation.ts +49 -0
  121. package/src/server/request-log.ts +2 -0
  122. package/src/server/responses/anthropic-thinking-recovery.ts +1 -0
  123. package/src/server/responses/compact.ts +10 -2
  124. package/src/server/responses/core-combo.ts +53 -31
  125. package/src/server/responses/core-lifetime.ts +2 -2
  126. package/src/server/responses/core-normalize.ts +6 -3
  127. package/src/server/responses/core-opaque-recovery.ts +1 -0
  128. package/src/server/responses/core-options.ts +5 -0
  129. package/src/server/responses/core-replay.ts +2 -1
  130. package/src/server/responses/jev-combo-choice.ts +39 -0
  131. package/src/server/responses/jev-session-pin.ts +203 -0
  132. package/src/server/responses/passthrough-delivery.ts +4 -0
  133. package/src/server/responses/request-prepare.ts +10 -5
  134. package/src/server/responses/request-transport.ts +25 -6
  135. package/src/server/responses/run-turn-execution.ts +2 -4
  136. package/src/server/responses/sidecar-execution.ts +2 -4
  137. package/src/server/responses-phase-inference.ts +267 -0
  138. package/src/server/sse-payload-rewrite.ts +14 -1
  139. package/src/service/definition.ts +20 -8
  140. package/src/types/config.ts +6 -0
  141. package/src/types/provider.ts +9 -1
  142. package/src/types/request.ts +1 -0
  143. package/src/types/wire.ts +1 -0
  144. package/src/update/codex-cli-update-launch-policy.d.mts +9 -0
  145. package/src/update/codex-cli-update-launch-policy.mjs +30 -0
  146. package/src/usage/jev-stats.ts +54 -14
  147. package/gui/dist/assets/index-DGXS51sp.js +0 -94
  148. package/gui/dist/assets/quota-summary-DO1Sx5TM.js +0 -1
package/src/cli/link.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  import { clientLinkStatusIdentity } from "../client/link-status";
2
2
  import { clientLinkTunnelStatus } from "../client/link-tunnel";
3
3
  import { loadConfig } from "../config";
4
- import { projectLinkStatus, type ClientLinkIdentity, type LinkStatusConfig, type LinkStatusDto } from "../link/status-projection";
4
+ import {
5
+ projectLinkStatus, type ClientLinkIdentity, type LinkStatusConfig, type LinkStatusDto, type LinkTunnelDiagnostics, type LinkWireState,
6
+ } from "../link/status-projection";
7
+ import { TUNNEL_CAUSES } from "../link/tunnel-history";
5
8
  import { isEpochMs, type LinkChildParity } from "../link/child-parity";
6
9
  import { CODEX_SETTINGS_DIGEST_PATTERN } from "../link/codex-settings";
7
10
  import type { LinkLocalPeer, LinkPeerStatus } from "../link/compatibility";
@@ -40,30 +43,11 @@ export const LINK_USAGE = `Usage:
40
43
  Forced revoke skips remote cleanup. Run ocx disconnect on the remote client.
41
44
  Cleanup finds Windows Children and explains how to move them to Hub mode; it changes nothing.`;
42
45
 
43
- type LinkState = "connecting" | "connected" | "reconnecting" | "failed" | "idle";
44
- type ListenerState = "off" | "listening" | "failed";
45
-
46
- export interface LinkStatusPayload {
47
- role: "standalone" | "home" | "child";
48
- listener: { state: ListenerState; port: number | null };
49
- links: Array<LinkChildParity & {
50
- id: string;
51
- alias: string;
52
- direction: LinkDirection;
53
- state: LinkState;
54
- since: string;
55
- reason: string | null;
56
- tunnelPort: number;
57
- peer?: LinkPeerStatus;
58
- }>;
59
- child: null | {
60
- alias: string;
61
- state: LinkState;
62
- since: string;
63
- reason: string | null;
64
- };
65
- local?: LinkLocalPeer;
66
- }
46
+ /**
47
+ * What `ocx link status` prints: the runtime's status DTO itself, so a field the server adds (like
48
+ * `peer` or the diagnostics `tunnel`) is part of this type the moment it is part of the wire.
49
+ */
50
+ export type LinkStatusPayload = LinkStatusDto;
67
51
 
68
52
  export interface LinkIssuePayload {
69
53
  linkId: string;
@@ -165,11 +149,11 @@ function validateStatus(value: unknown): LinkStatusPayload {
165
149
  throw new Error("invalid link API response: listener port");
166
150
  }
167
151
  if (!Array.isArray(value.links)) throw new Error("invalid link API response: links");
168
- const links = value.links.map((candidate, index) => {
152
+ const links = value.links.map((candidate, index): LinkStatusPayload["links"][number] => {
169
153
  if (!isRecord(candidate)) throw new Error(`invalid link API response: link ${index}`);
170
154
  // The OCX-6 parity fields are optional on the wire (an older runtime omits them) and always
171
155
  // present in this output, null when absent.
172
- assertKeys(candidate, ["id", "alias", "direction", "state", "since", "reason", "tunnelPort"], [...PARITY_KEYS, "peer"], `link ${index}`);
156
+ assertKeys(candidate, ["id", "alias", "direction", "state", "since", "reason", "tunnelPort"], [...PARITY_KEYS, "peer", "tunnel"], `link ${index}`);
173
157
  const parity = validateParity(candidate, index);
174
158
  if (typeof candidate.id !== "string" || !LINK_ID.test(candidate.id)
175
159
  || !validString(candidate.alias) || (candidate.direction !== "hub-initiated" && candidate.direction !== "client-initiated")
@@ -180,14 +164,15 @@ function validateStatus(value: unknown): LinkStatusPayload {
180
164
  return {
181
165
  id: candidate.id,
182
166
  alias: candidate.alias,
183
- direction: candidate.direction,
184
- state: candidate.state,
167
+ direction: candidate.direction as LinkDirection,
168
+ state: candidate.state as LinkWireState,
185
169
  since: candidate.since,
186
170
  reason: candidate.reason,
187
171
  tunnelPort: candidate.tunnelPort,
188
172
  ...parity,
189
173
  ...(candidate.peer !== undefined ? { peer: validatePeer(candidate.peer, index) } : {}),
190
- } as LinkStatusPayload["links"][number];
174
+ ...(candidate.tunnel !== undefined ? { tunnel: validateTunnel(candidate.tunnel, index) } : {}),
175
+ };
191
176
  });
192
177
  let child: LinkStatusPayload["child"] = null;
193
178
  if (value.child !== null) {
@@ -310,8 +295,9 @@ async function runStatus(args: string[], deps: LinkCliDeps): Promise<void> {
310
295
  }
311
296
  throw error;
312
297
  }
313
- // A proxy older than the compatibility report ignores the query and answers the exact K16 shape.
314
- const result = validateStatus(await linkRequest<unknown>("/api/link/status?compatibility=1", {}, { ...deps, baseUrl }));
298
+ // A proxy older than the compatibility report or the tunnel diagnostics ignores the query and
299
+ // answers without those fields.
300
+ const result = validateStatus(await linkRequest<unknown>("/api/link/status?compatibility=1&diagnostics=1", {}, { ...deps, baseUrl }));
315
301
  console.log(JSON.stringify(result));
316
302
  }
317
303
 
@@ -385,6 +371,33 @@ export async function runLinkCommand(rawArgs: string[], deps: LinkCliDeps = {}):
385
371
  }
386
372
 
387
373
  const PARITY_KEYS = ["lastApplyAt", "runtimeStartedAt", "appliedCodexSettingsDigest", "reportedAt"] as const;
374
+ const WIRE_STATES = new Set(["connecting", "connected", "reconnecting", "failed", "idle"]);
375
+ const CAUSES = new Set<string>(TUNNEL_CAUSES);
376
+
377
+ function validIso(value: unknown): value is string {
378
+ return typeof value === "string" && value.length <= 64 && Number.isFinite(Date.parse(value));
379
+ }
380
+
381
+ function validateTunnel(value: unknown, index: number): LinkTunnelDiagnostics {
382
+ const fail = (): never => { throw new Error(`invalid link API response: link ${index} tunnel`); };
383
+ if (!isRecord(value)) return fail();
384
+ assertExactKeys(value, ["attempt", "nextRetryAt", "lastTransition", "lastFailure"], `link ${index} tunnel`);
385
+ const { attempt, nextRetryAt, lastTransition, lastFailure } = value;
386
+ if (typeof attempt !== "number" || !Number.isSafeInteger(attempt) || attempt < 0) fail();
387
+ if (nextRetryAt !== null && !validIso(nextRetryAt)) fail();
388
+ if (lastTransition !== null) {
389
+ if (!isRecord(lastTransition)) return fail();
390
+ assertExactKeys(lastTransition, ["from", "to", "at", "cause"], `link ${index} tunnel transition`);
391
+ if (!WIRE_STATES.has(lastTransition.from as string) || !WIRE_STATES.has(lastTransition.to as string)
392
+ || !validIso(lastTransition.at) || (lastTransition.cause !== null && !CAUSES.has(lastTransition.cause as string))) fail();
393
+ }
394
+ if (lastFailure !== null) {
395
+ if (!isRecord(lastFailure)) return fail();
396
+ assertExactKeys(lastFailure, ["cause", "at"], `link ${index} tunnel failure`);
397
+ if (!CAUSES.has(lastFailure.cause as string) || !validIso(lastFailure.at)) fail();
398
+ }
399
+ return value as unknown as LinkTunnelDiagnostics;
400
+ }
388
401
 
389
402
  function validateParity(candidate: Record<string, unknown>, index: number): LinkChildParity {
390
403
  const time = (key: "lastApplyAt" | "runtimeStartedAt" | "reportedAt"): number | null => {
@@ -191,7 +191,7 @@ function handleList(args: string[], active: boolean = localProvidersActive()): v
191
191
  console.log(` ${name}${isDefault}${source} adapter=${prov.adapter}${model}`);
192
192
  }
193
193
 
194
- const available = PROVIDER_REGISTRY.filter(e => !configured.includes(e.id));
194
+ const available = PROVIDER_REGISTRY.filter(e => !e.deprecated && !configured.includes(e.id));
195
195
  if (available.length > 0) {
196
196
  console.log(`\nAvailable from registry (${available.length}):\n`);
197
197
  for (const entry of available) {
@@ -265,6 +265,7 @@ async function handleAdd(args: string[], deps: ProviderCommandDeps): Promise<voi
265
265
  const registryEntry = getProviderRegistryEntry(name);
266
266
 
267
267
  if (registryEntry) {
268
+ if (registryEntry.deprecated) console.warn(`Warning: provider "${name}" is deprecated: ${registryEntry.deprecated}`);
268
269
  provConfig = providerConfigSeed(registryEntry);
269
270
  if (apiKey) {
270
271
  const candidateError = providerCredentialInputError({ apiKey });
@@ -8,7 +8,11 @@ import { readClientConnectionState, sameClientConnectionOwner } from "../client/
8
8
  import { fetchHubUsage } from "../client/hub-client";
9
9
  import { readServiceApiTokenState, serviceApiTokenFingerprint } from "../lib/service-secrets";
10
10
  import { runningProxyUpdateHeaders } from "../oauth/login-cli";
11
+ import { readRuntimePort, type RuntimePortState } from "../config/process-state";
12
+ import { isLocalAttestationSecret } from "../lib/local-management-attestation";
11
13
  import { redactSecretString } from "../lib/redact";
14
+ import { findLiveProxy, probeHostname } from "../server/proxy-liveness";
15
+ import { fetchBoundLocalManagementRead, type LocalManagementReadDeps } from "../server/local-management-read-client";
12
16
  import { rangeWindow, USAGE_SURFACES } from "../usage/summary";
13
17
  import type { UsageTimeWindow } from "../usage/time-range";
14
18
  import {
@@ -16,8 +20,7 @@ import {
16
20
  printData,
17
21
  rejectArgs,
18
22
  RuntimeApiError,
19
- runtimeRequest,
20
- runtimeBaseUrl,
23
+ PROXY_NOT_RUNNING_MESSAGE,
21
24
  takeFlag,
22
25
  takeOption,
23
26
  takeOptionWithSyntax,
@@ -29,7 +32,10 @@ export const USAGE_COMPARE_USAGE = `Usage:
29
32
  [--provider <name>] [--model <id>] [--api-key-id <id>] [--json]`;
30
33
 
31
34
  export type CompareRange = "today" | "7d" | "30d";
32
- export type UsageCompareDeps = RuntimeApiDeps & { now?: () => number };
35
+ export type UsageCompareDeps = RuntimeApiDeps & Pick<LocalManagementReadDeps, "directFetch"> & {
36
+ now?: () => number;
37
+ readRuntime?: typeof readRuntimePort;
38
+ };
33
39
 
34
40
  const METRICS = ["requests", "totalTokens", "inputTokens", "outputTokens", "cachedInputTokens", "estimatedCostUsd"] as const;
35
41
  type Metric = typeof METRICS[number];
@@ -189,10 +195,60 @@ function captureUsageOwner() {
189
195
  const headers = runningProxyUpdateHeaders();
190
196
  const fingerprint = connection.kind === "connected" ? connection.value.tokenFingerprint
191
197
  : serviceApiTokenFingerprint(headers.get("X-OpenCodex-API-Key") ?? "");
192
- return { connection, token, headers, fingerprint };
198
+ return { connection, token, fingerprint };
193
199
  }
194
200
  type UsageOwner = ReturnType<typeof captureUsageOwner>;
195
201
 
202
+ function pinUsageRuntime(deps: UsageCompareDeps, owner: UsageOwner): (suffix: string) => Promise<ScopedUsage> {
203
+ const readRuntime = deps.readRuntime ?? readRuntimePort;
204
+ const initial = readRuntimePortForComparison();
205
+ const pinned = { ...initial };
206
+ let liveIdentity: string | undefined;
207
+ function readRuntimePortForComparison(): RuntimePortState {
208
+ const runtime = readRuntime();
209
+ if (!runtime || !isLocalAttestationSecret(runtime.attestationSecret)) {
210
+ throw new Error("Usage runtime unavailable: restart the proxy with an attested runtime record.");
211
+ }
212
+ return runtime;
213
+ }
214
+ function assertRuntime(): RuntimePortState {
215
+ const current = readRuntimePortForComparison();
216
+ if (current.pid !== pinned.pid || current.port !== pinned.port || current.hostname !== pinned.hostname
217
+ || current.attestationSecret !== pinned.attestationSecret) {
218
+ throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
219
+ }
220
+ return current;
221
+ }
222
+ return async suffix => {
223
+ assertRuntime();
224
+ const live = await (deps.findLiveProxy ?? findLiveProxy)();
225
+ assertRuntime();
226
+ assertUsageOwner(owner);
227
+ if (!live) throw new RuntimeApiError(PROXY_NOT_RUNNING_MESSAGE, 503, null, "proxy_not_running");
228
+ const identity = JSON.stringify([live.pid, live.port, live.hostname, live.source, live.role]);
229
+ if (live.source !== "runtime" || live.pid !== pinned.pid || live.port !== pinned.port
230
+ || probeHostname(live.hostname) !== probeHostname(pinned.hostname) || live.role === "client"
231
+ || (liveIdentity !== undefined && liveIdentity !== identity)) {
232
+ throw new Error("Usage runtime changed during comparison; no comparison was produced. Retry for the current runtime.");
233
+ }
234
+ liveIdentity = identity;
235
+ const read = await fetchBoundLocalManagementRead(live, `/api/usage?${suffix.replace(/^\?/, "")}`, {
236
+ fetchImpl: deps.fetchImpl, directFetch: deps.directFetch, readRuntime: assertRuntime,
237
+ timeoutMs: 60_000, requireResponseProof: true,
238
+ });
239
+ assertRuntime();
240
+ if (read.kind === "unavailable") {
241
+ throw new RuntimeApiError(`Local usage read unavailable (${read.reason}). No comparison was produced.`, 503, null);
242
+ }
243
+ if (!read.response.ok) {
244
+ throw new RuntimeApiError(`Usage request failed (HTTP ${read.response.status}). No comparison was produced.`, read.response.status, null);
245
+ }
246
+ const result = await read.response.json() as ScopedUsage;
247
+ assertRuntime();
248
+ return result;
249
+ };
250
+ }
251
+
196
252
  function assertUsageOwner(owner: UsageOwner): void {
197
253
  const current = readClientConnectionState();
198
254
  const expected = owner.connection;
@@ -216,6 +272,7 @@ async function readUsage(
216
272
  apiKeyId: string | undefined,
217
273
  deps: RuntimeApiDeps,
218
274
  owner: UsageOwner,
275
+ localRead?: (suffix: string) => Promise<ScopedUsage>,
219
276
  ): Promise<ScopedUsage> {
220
277
  assertUsageOwner(owner);
221
278
  const { connection, token } = owner;
@@ -233,16 +290,15 @@ async function readUsage(
233
290
  result = await fetchHubUsage(connection.value.serverUrl, token.token, new URLSearchParams(suffix.replace(/^\?/, "")), {
234
291
  fetchImpl: peerFetch, timeoutMs: 60_000, ...(link ? { peer: "Home" as const } : {}),
235
292
  });
236
- } else if (apiKeyId === undefined) {
237
- result = await runtimeRequest(`/api/usage${suffix}`, { headers: owner.headers }, deps);
238
293
  } else {
239
294
  try {
240
- result = await runtimeRequest(`/api/usage${suffix}`, { redirect: "error", credentials: "omit", headers: owner.headers }, deps);
295
+ result = await localRead!(suffix);
241
296
  } catch (error) {
242
- if (error instanceof RuntimeApiError && error.code === "proxy_not_running") throw error;
297
+ if (apiKeyId === undefined) throw error;
298
+ if (error instanceof RuntimeApiError) throw error;
243
299
  throw new Error("Key-scoped usage could not be read. Check runtime access and retry.");
244
300
  }
245
- if (result.filter?.apiKeyId !== apiKeyId) {
301
+ if (apiKeyId !== undefined && result.filter?.apiKeyId !== apiKeyId) {
246
302
  throw new Error("The server did not confirm the requested API key scope. Upgrade and restart the proxy, then retry.");
247
303
  }
248
304
  }
@@ -276,9 +332,9 @@ export async function handleUsageCompareCommand(argv: string[], deps: UsageCompa
276
332
  const windows = compareUsageWindows(range, now);
277
333
  const filters = { range, surface, provider, model, apiKeyId };
278
334
  const owner = captureUsageOwner();
279
- const readDeps = owner.connection.kind === "disconnected" ? { ...deps, baseUrl: await runtimeBaseUrl(deps) } : deps;
280
- const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, readDeps, owner);
281
- const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, readDeps, owner);
335
+ const localRead = owner.connection.kind === "disconnected" ? pinUsageRuntime(deps, owner) : undefined;
336
+ const currentReport = await readUsage(query({ ...filters, since: windows.current.since, until: windows.current.until }), windows.current, apiKeyId, deps, owner, localRead);
337
+ const previousReport = await readUsage(query({ ...filters, since: windows.previous.since, until: windows.previous.until }), windows.previous, apiKeyId, deps, owner, localRead);
282
338
  const current = totalsOf(currentReport);
283
339
  const previous = totalsOf(previousReport);
284
340
  const deltaTotals: Totals = {};
@@ -76,7 +76,7 @@ function liftRequestIdleTimer(req: Request, server: Pick<Server<unknown>, "timeo
76
76
  * fetch, so a rebinding or cross-site browser page cannot reach the Home through this port.
77
77
  * - Without the committed link key nothing is fetched, and the caller gets
78
78
  * `503 link_credential_unavailable`.
79
- * - `/readyz` stays local: it reports this listener, not the tunnel.
79
+ * - `/readyz` stays local and is never relayed; the listener answers it from `link-readiness.ts`.
80
80
  * - A relayed request is exempt from the listener's idle limit, like a standalone data route.
81
81
  */
82
82
  export function handleLinkIngress(
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Whether a Remote Link Child can serve relayed requests right now, for its `GET /readyz`.
3
+ *
4
+ * `/healthz` keeps meaning "this process answers". A Child whose tunnel is down still answers it,
5
+ * yet every relayed turn fails with 503, so readiness is where that shows: not ready, with one
6
+ * fixed reason word. Nothing here carries ssh output, host names, ports or keys.
7
+ *
8
+ * - A Child-initiated link reads its own tunnel supervisor; no network traffic.
9
+ * - A Home-initiated link has no local tunnel to inspect (the Home runs `ssh -R`), so it runs
10
+ * the credential-free relay preflight through the tunnel port. One preflight runs at a time, a
11
+ * verdict is reused for `LINK_READINESS_TTL_MS`, and a request waits at most
12
+ * `LINK_READINESS_WAIT_MS` for a running preflight before it answers `link_unverified`. An
13
+ * unauthenticated caller can therefore cause at most one preflight per TTL.
14
+ */
15
+ import { LINK_RELAY_AUTH_TIMEOUT_MS } from "../link/relay-auth";
16
+ import { isLinkPort } from "../link/ports";
17
+ import { probeLinkRelayPreflight, type BoundLinkRelayTarget, type LinkRelayPreflightResult } from "./link-relay-transport";
18
+ import type { ClientLinkSupervisorStatus } from "./link-tunnel";
19
+
20
+ export type LinkReadinessReason =
21
+ | "tunnel_idle" | "tunnel_connecting" | "tunnel_failed" | "tunnel_unavailable" | "sidecar_invalid"
22
+ | "home_unreachable" | "home_unauthorized" | "link_auth_failed" | "link_credential_unavailable"
23
+ | "link_unverified";
24
+
25
+ export type LinkReadiness = { ready: true } | { ready: false; reason: LinkReadinessReason };
26
+
27
+ export const LINK_READINESS_TTL_MS = 5_000;
28
+ export const LINK_READINESS_WAIT_MS = 500;
29
+
30
+ const READY: LinkReadiness = { ready: true };
31
+ const notReady = (reason: LinkReadinessReason): LinkReadiness => ({ ready: false, reason });
32
+
33
+ /**
34
+ * A Child-initiated link from its supervisor. The keyed probe the supervisor runs every 30 seconds
35
+ * while connected is display-only for the tunnel, but a Home that stopped answering or refuses the
36
+ * key cannot serve a turn, so readiness follows it. A Home whose own startup readiness is pending
37
+ * still serves relayed requests (the probe's `home_not_ready`), so that reads ready.
38
+ */
39
+ export function clientInitiatedLinkReadiness(status: ClientLinkSupervisorStatus): LinkReadiness {
40
+ if (status.kind === "failed") return notReady("sidecar_invalid");
41
+ if (status.kind === "stopped") return notReady("tunnel_idle");
42
+ const { state, probe } = status;
43
+ if (state.kind === "connected") {
44
+ if (probe === "home_unreachable") return notReady("home_unreachable");
45
+ if (probe === "unauthorized") return notReady("home_unauthorized");
46
+ return READY;
47
+ }
48
+ if (state.kind === "connecting" || state.kind === "reconnecting") return notReady("tunnel_connecting");
49
+ if (state.kind === "failed") return notReady(state.inFlight ? "tunnel_connecting" : "tunnel_failed");
50
+ return notReady("tunnel_idle");
51
+ }
52
+
53
+ function fromPreflight(result: LinkRelayPreflightResult): LinkReadiness {
54
+ if (result === "ok") return READY;
55
+ return notReady(result === "unreachable" ? "home_unreachable" : "link_auth_failed");
56
+ }
57
+
58
+ export interface HomeInitiatedLinkReadinessDeps {
59
+ target: Omit<BoundLinkRelayTarget, "admissionKey">;
60
+ linkKey: () => string | null;
61
+ probe?: (target: BoundLinkRelayTarget, options: { timeoutMs: number }) => Promise<LinkRelayPreflightResult>;
62
+ now?: () => number;
63
+ /** Resolves after `ms`; the wait for a running preflight. */
64
+ delay?: (ms: number) => Promise<void>;
65
+ }
66
+
67
+ /** The readiness of a Home-initiated link, from a shared, rate-limited relay preflight. */
68
+ export function createHomeInitiatedLinkReadiness(deps: HomeInitiatedLinkReadinessDeps): () => Promise<LinkReadiness> {
69
+ const probe = deps.probe ?? probeLinkRelayPreflight;
70
+ const now = deps.now ?? Date.now;
71
+ const delay = deps.delay ?? (ms => new Promise<void>(resolve => {
72
+ const timer = setTimeout(resolve, ms);
73
+ (timer as { unref?: () => void }).unref?.();
74
+ }));
75
+ let last: { at: number; verdict: LinkReadiness } | undefined;
76
+ let flight: Promise<LinkReadiness> | undefined;
77
+ const run = (admissionKey: string): Promise<LinkReadiness> => {
78
+ flight ??= probe({ ...deps.target, admissionKey }, { timeoutMs: LINK_RELAY_AUTH_TIMEOUT_MS })
79
+ .then(fromPreflight, () => notReady("home_unreachable"))
80
+ .then(verdict => {
81
+ last = { at: now(), verdict };
82
+ return verdict;
83
+ })
84
+ .finally(() => { flight = undefined; });
85
+ return flight;
86
+ };
87
+ return async () => {
88
+ if (!isLinkPort(deps.target.tunnelPort)) return notReady("tunnel_unavailable");
89
+ let admissionKey: string | null;
90
+ try { admissionKey = deps.linkKey(); } catch { admissionKey = null; }
91
+ if (!admissionKey) return notReady("link_credential_unavailable");
92
+ const at = now();
93
+ if (last && at >= last.at && at - last.at < LINK_READINESS_TTL_MS) return last.verdict;
94
+ const unverified = notReady("link_unverified");
95
+ return await Promise.race([run(admissionKey), delay(LINK_READINESS_WAIT_MS).then(() => unverified)]);
96
+ };
97
+ }
@@ -9,7 +9,7 @@ import { linkPeerReportHeaders } from "../link/peer-report";
9
9
  import { buildPeerFeatures } from "../remote/features";
10
10
  import {
11
11
  LINK_RELAY_AUTH_PATH, LINK_RELAY_AUTH_TIMEOUT_MS, LINK_RELAY_AUTH_VERSION,
12
- LINK_RELAY_SESSION_HEADER, linkRelayChallenge, linkRelayProofMatches,
12
+ LINK_RELAY_PROBE_HEADER, LINK_RELAY_SESSION_HEADER, linkRelayChallenge, linkRelayProofMatches,
13
13
  } from "../link/relay-auth";
14
14
 
15
15
  export interface BoundLinkRelayTarget {
@@ -27,6 +27,14 @@ export class LinkRelayAuthenticationError extends Error {
27
27
  }
28
28
  }
29
29
 
30
+ /** Only the credential-free preflight deadline expired; no data request has begun. */
31
+ export class LinkRelayAuthTimeoutError extends Error {
32
+ constructor() {
33
+ super("link relay authentication timed out");
34
+ this.name = "LinkRelayAuthTimeoutError";
35
+ }
36
+ }
37
+
30
38
  /** One physical connection, ever. A closed keep-alive socket cannot trigger a silent reconnect. */
31
39
  class SingleConnectionAgent extends Agent {
32
40
  socket: Socket | undefined;
@@ -153,21 +161,76 @@ async function responseOnConnection(incoming: IncomingMessage, agent: SingleConn
153
161
 
154
162
  let peerReport: Record<string, string> | undefined;
155
163
 
156
- /** Authenticate and stream one request on the SAME socket; never fall back to an ordinary fetch. */
157
- export async function fetchBoundLinkRelay(target: BoundLinkRelayTarget, destination: string, init: RequestInit): Promise<Response> {
164
+ /** The credential-free challenge for one connection; the proof binds this nonce to the link key. */
165
+ function relayChallenge(target: BoundLinkRelayTarget, origin: URL) {
166
+ const challenge = linkRelayChallenge(serviceApiTokenFingerprint(target.admissionKey), target.apiKeyId!, target.linkId!);
167
+ const probe = new URL(LINK_RELAY_AUTH_PATH, origin);
168
+ probe.search = new URLSearchParams({ version: LINK_RELAY_AUTH_VERSION, key: target.apiKeyId!,
169
+ link: target.linkId!, nonce: challenge.nonce, proof: challenge.caller }).toString();
170
+ // The challenge also carries this Child's version and features for the Home's link status.
171
+ peerReport ??= linkPeerReportHeaders(packageVersion("0.0.0"), buildPeerFeatures());
172
+ return { challenge, probe, headers: peerReport };
173
+ }
174
+
175
+ /**
176
+ * The outcome of the credential-free preflight alone: `ok` when the Home answered with a proof
177
+ * only the holder of the link key can make, `unrecognized` for a peer without the route (404),
178
+ * `auth_failed` for any other answer, and `unreachable` when nothing answered in time.
179
+ */
180
+ export type LinkRelayPreflightResult = "ok" | "unreachable" | "auth_failed" | "unrecognized";
181
+
182
+ /**
183
+ * Run only the preflight a relayed request starts with, on its own connection, and close it. It
184
+ * sends no link key, no data request and no body; the Child's readiness uses it to learn whether
185
+ * the Home answers through the tunnel right now. It is marked probe-only, so the Home reserves no
186
+ * session for a data request that never comes. The response body is never read.
187
+ */
188
+ export async function probeLinkRelayPreflight(
189
+ target: BoundLinkRelayTarget, options: { timeoutMs?: number } = {},
190
+ ): Promise<LinkRelayPreflightResult> {
191
+ const timeoutMs = options.timeoutMs ?? LINK_RELAY_AUTH_TIMEOUT_MS;
192
+ if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) throw new RangeError("invalid link relay preflight timeout");
193
+ if (!target.linkId || !target.apiKeyId) return "auth_failed";
194
+ const origin = new URL(`http://127.0.0.1:${target.tunnelPort}`);
195
+ let request: ReturnType<typeof relayChallenge>;
196
+ try { request = relayChallenge(target, origin); } catch { return "auth_failed"; }
197
+ const agent = new SingleConnectionAgent({ keepAlive: false, maxSockets: 1, maxFreeSockets: 0 });
198
+ const deadline = clearableDeadline(timeoutMs);
199
+ try {
200
+ const response = await requestOnConnection(request.probe, {
201
+ method: "GET", signal: deadline.signal, headers: { ...request.headers, [LINK_RELAY_PROBE_HEADER]: "1" },
202
+ }, agent);
203
+ const proof = response.headers["x-opencodex-link-proof"];
204
+ if (response.statusCode === 204 && typeof proof === "string" && linkRelayProofMatches(proof, request.challenge.expected)) return "ok";
205
+ return response.statusCode === 404 ? "unrecognized" : "auth_failed";
206
+ } catch {
207
+ return "unreachable";
208
+ } finally {
209
+ deadline.clear();
210
+ await agent.close();
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Authenticate and stream one request on the SAME socket; never fall back to an ordinary fetch.
216
+ * LinkRelayAuthTimeoutError certifies that only the credential-free preflight expired before
217
+ * data-request construction. Caller aborts and failures after that boundary keep their own errors.
218
+ */
219
+ export async function fetchBoundLinkRelay(
220
+ target: BoundLinkRelayTarget, destination: string, init: RequestInit,
221
+ options: { authTimeoutMs?: number } = {},
222
+ ): Promise<Response> {
223
+ const authTimeoutMs = options.authTimeoutMs ?? LINK_RELAY_AUTH_TIMEOUT_MS;
224
+ if (!Number.isFinite(authTimeoutMs) || authTimeoutMs <= 0) throw new RangeError("invalid link relay auth timeout");
158
225
  if (!target.linkId || !target.apiKeyId) throw new LinkRelayAuthenticationError();
159
226
  const url = new URL(destination);
160
227
  if (url.origin !== `http://127.0.0.1:${target.tunnelPort}`) throw new LinkRelayAuthenticationError();
161
- const challenge = linkRelayChallenge(serviceApiTokenFingerprint(target.admissionKey), target.apiKeyId, target.linkId);
162
- const probe = new URL(LINK_RELAY_AUTH_PATH, url);
163
- probe.search = new URLSearchParams({ version: LINK_RELAY_AUTH_VERSION, key: target.apiKeyId,
164
- link: target.linkId, nonce: challenge.nonce, proof: challenge.caller }).toString();
228
+ const { challenge, probe, headers: preflightHeaders } = relayChallenge(target, url);
165
229
  const agent = new SingleConnectionAgent({ keepAlive: true, maxSockets: 1, maxFreeSockets: 1 });
166
- const deadline = clearableDeadline(LINK_RELAY_AUTH_TIMEOUT_MS, init.signal ?? undefined);
230
+ const deadline = clearableDeadline(authTimeoutMs, init.signal ?? undefined);
231
+ let dataRequestStarted = false;
167
232
  try {
168
- // The challenge also carries this Child's version and features for the Home's link status.
169
- peerReport ??= linkPeerReportHeaders(packageVersion("0.0.0"), buildPeerFeatures());
170
- const response = await requestOnConnection(probe, { method: "GET", signal: deadline.signal, headers: peerReport }, agent);
233
+ const response = await requestOnConnection(probe, { method: "GET", signal: deadline.signal, headers: preflightHeaders }, agent);
171
234
  const socket = agent.socket;
172
235
  const proof = response.headers["x-opencodex-link-proof"];
173
236
  if (response.statusCode !== 204 || typeof proof !== "string" || !linkRelayProofMatches(proof, challenge.expected)) {
@@ -177,15 +240,20 @@ export async function fetchBoundLinkRelay(target: BoundLinkRelayTarget, destinat
177
240
  for await (const chunk of response) {
178
241
  if (chunk.length) throw new LinkRelayAuthenticationError();
179
242
  }
243
+ deadline.signal.throwIfAborted();
180
244
  deadline.clear();
181
245
  init.signal?.throwIfAborted();
182
246
  if (!socket || socket.destroyed || socket !== agent.socket) throw new LinkRelayAuthenticationError();
183
247
  const headers = new Headers(init.headers);
184
248
  headers.set(LINK_RELAY_SESSION_HEADER, challenge.nonce);
249
+ dataRequestStarted = true;
185
250
  const incoming = await requestOnConnection(url, { ...init, headers }, agent, socket);
186
251
  return await responseOnConnection(incoming, agent, init.method ?? "GET");
187
252
  } catch (error) {
253
+ const authDeadlineExpired = !dataRequestStarted && deadline.didExpire()
254
+ && !(error instanceof LinkRelayAuthenticationError);
188
255
  await agent.close();
256
+ if (authDeadlineExpired && !init.signal?.aborted) throw new LinkRelayAuthTimeoutError();
189
257
  throw error;
190
258
  } finally { deadline.clear(); }
191
259
  }