@bitkyc08/opencodex 2.11.1 → 2.12.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 (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
@@ -0,0 +1,77 @@
1
+ import { createHash } from "node:crypto";
2
+ import { jcsStringify } from "../digest";
3
+ import type { LabBehaviorSource, LabBehaviorValues } from "../live/types";
4
+
5
+ const CLOSED_KEYS = new Set([
6
+ "wire.adapter", "wire.upstreamProtocol", "wire.responsesPath", "wire.commandCodeVersion", "wire.modelSuffixMode",
7
+ "auth.mode", "auth.transport",
8
+ "responses.stateful", "responses.upstreamStreaming", "responses.serviceTier", "responses.snapshotRepair", "responses.itemIdRepair",
9
+ "limits.contextWindow", "limits.maxInputTokens", "limits.maxOutputTokens",
10
+ "modalities.input",
11
+ "sampling.omitTemperature", "sampling.omitTopP", "sampling.omitPenalties",
12
+ "reasoning.supported", "reasoning.efforts", "reasoning.defaultEffort", "reasoning.effortMap", "reasoning.wireFormat",
13
+ "reasoning.summaryMode", "reasoning.replayMode", "reasoning.splitMode", "reasoning.toggleMode", "reasoning.budgetMode",
14
+ "tools.choiceRestrictions", "tools.parallel", "tools.hostedPreference", "tools.customFreeform", "tools.builtinNameEscaping",
15
+ "cache.forwarding", "cache.retention", "anthropic.eofPolicy",
16
+ "google.mode", "google.projectFingerprint", "google.locationFingerprint",
17
+ "openrouter.order", "openrouter.only", "openrouter.allowFallbacks",
18
+ "sidecars.vision", "sidecars.webSearch",
19
+ "mcp.maxTools", "mcp.maxSchemaBytes", "mcp.maxResultBytes", "mcp.nativeLocalExec",
20
+ "runtime.bunVersion", "runtime.platform", "runtime.arch", "runtime.streamMode", "runtime.fastMode", "runtime.effortCap",
21
+ "headers.nonCredentialBehaviorDigest",
22
+ ] as const);
23
+
24
+ const SOURCES = new Set<LabBehaviorSource>([
25
+ "request", "model_override", "provider_config", "registry_runtime_default", "generated_model_metadata",
26
+ "global_config", "adapter_default", "lab_forced",
27
+ ]);
28
+
29
+ const SET_ARRAY_KEYS = new Set(["modalities.input", "reasoning.efforts", "tools.choiceRestrictions", "openrouter.only"]);
30
+ const ORDERED_ARRAY_KEYS = new Set(["openrouter.order"]);
31
+ const REQUIRED_KEYS = [
32
+ "wire.adapter", "wire.upstreamProtocol", "auth.mode", "auth.transport", "mcp.nativeLocalExec",
33
+ "runtime.bunVersion", "runtime.platform", "runtime.arch", "runtime.streamMode", "runtime.fastMode", "runtime.effortCap",
34
+ ] as const;
35
+
36
+ function canonicalArray(key: string, value: unknown[]): unknown[] {
37
+ if (SET_ARRAY_KEYS.has(key)) {
38
+ const byBytes = value.map((item) => ({ item, bytes: jcsStringify(item) }));
39
+ const seen = new Set<string>();
40
+ for (const row of byBytes) {
41
+ if (seen.has(row.bytes)) throw new Error(`harness_failure: duplicate behavior array value for ${key}`);
42
+ seen.add(row.bytes);
43
+ }
44
+ byBytes.sort((a, b) => a.bytes < b.bytes ? -1 : a.bytes > b.bytes ? 1 : 0);
45
+ return byBytes.map((row) => row.item);
46
+ }
47
+ if (ORDERED_ARRAY_KEYS.has(key)) return [...value];
48
+ throw new Error(`harness_failure: unclassified_behavior_input array for ${key}`);
49
+ }
50
+
51
+ export function normalizeBehaviorValues(values: LabBehaviorValues): LabBehaviorValues {
52
+ if (!values || typeof values !== "object" || Array.isArray(values)) {
53
+ throw new Error("harness_failure: behavior resolver output is required");
54
+ }
55
+ for (const key of REQUIRED_KEYS) {
56
+ if (!(key in values)) throw new Error(`harness_failure: unclassified_behavior_input missing ${key}`);
57
+ }
58
+ const normalized: LabBehaviorValues = {};
59
+ for (const key of Object.keys(values).sort()) {
60
+ if (!CLOSED_KEYS.has(key as never)) throw new Error(`harness_failure: unclassified_behavior_input ${key}`);
61
+ const row = values[key];
62
+ if (!row || typeof row !== "object" || Array.isArray(row) || !SOURCES.has(row.source)) {
63
+ throw new Error(`harness_failure: invalid behavior source for ${key}`);
64
+ }
65
+ normalized[key] = {
66
+ source: row.source,
67
+ value: Array.isArray(row.value) ? canonicalArray(key, row.value) : row.value,
68
+ };
69
+ }
70
+ return normalized;
71
+ }
72
+
73
+ /** Hash the authoritative production resolver output; Lab performs validation/canonicalization only. */
74
+ export function buildBehaviorFingerprintV1(values: LabBehaviorValues): string {
75
+ const payload = { schemaVersion: 1, resolverVersion: 1, values: normalizeBehaviorValues(values) };
76
+ return createHash("sha256").update(jcsStringify(payload)).digest("hex");
77
+ }
@@ -0,0 +1,81 @@
1
+ import { closeSync, fsyncSync, linkSync, mkdirSync, openSync, readFileSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { randomBytes } from "node:crypto";
3
+ import { dirname } from "node:path";
4
+ import { labInstallationSaltPath, labRoot } from "../paths";
5
+
6
+ const SALT_BYTES = 32;
7
+ const UNSUPPORTED_DIRECTORY_FSYNC_CODES = new Set(["EINVAL", "ENOTSUP", "EOPNOTSUPP", "ENOSYS"]);
8
+
9
+ function readSaltFile(path: string): Uint8Array {
10
+ const bytes = readFileSync(path);
11
+ if (bytes.byteLength !== SALT_BYTES) {
12
+ throw new Error("harness_failure: invalid installation salt length");
13
+ }
14
+ return new Uint8Array(bytes);
15
+ }
16
+
17
+ function removeStagingFile(path: string): void {
18
+ try { unlinkSync(path); }
19
+ catch (error) {
20
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
21
+ }
22
+ }
23
+
24
+ function fsyncDirectory(path: string): void {
25
+ // Windows has no portable directory-fsync primitive. Some POSIX-backed
26
+ // filesystems (notably virtual/shared filesystems) report directory fsync as
27
+ // unsupported. In those cases the fsync'd staging inode plus atomic same-dir
28
+ // hard-link publication is the supported durability fallback. Unexpected I/O
29
+ // errors remain fatal.
30
+ if (process.platform === "win32") return;
31
+ const dirFd = openSync(path, "r");
32
+ try {
33
+ try { fsyncSync(dirFd); }
34
+ catch (error) {
35
+ const code = (error as NodeJS.ErrnoException).code;
36
+ if (code && UNSUPPORTED_DIRECTORY_FSYNC_CODES.has(code)) return;
37
+ throw error;
38
+ }
39
+ } finally { closeSync(dirFd); }
40
+ }
41
+
42
+ /** Read or atomically publish the per-installation salt used for local fingerprinting. */
43
+ export function readInstallationSalt(configDir?: string): Uint8Array {
44
+ const path = labInstallationSaltPath(configDir);
45
+ const root = labRoot(configDir);
46
+ mkdirSync(root, { recursive: true, mode: 0o700 });
47
+
48
+ try { return readSaltFile(path); }
49
+ catch (error) {
50
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
51
+ }
52
+
53
+ // The final pathname must never become visible before all 32 bytes are durable in the
54
+ // staging inode. A hard-link publish is atomic and fails with EEXIST when another process won.
55
+ const stagingPath = `${path}.tmp-${process.pid}-${randomBytes(8).toString("hex")}`;
56
+ if (dirname(stagingPath) !== dirname(path)) throw new Error("harness_failure: installation salt staging path escaped directory");
57
+ const salt = randomBytes(SALT_BYTES);
58
+ let fd: number | undefined;
59
+ try {
60
+ fd = openSync(stagingPath, "wx", 0o600);
61
+ writeFileSync(fd, salt);
62
+ fsyncSync(fd);
63
+ closeSync(fd);
64
+ fd = undefined;
65
+
66
+ try {
67
+ linkSync(stagingPath, path);
68
+ try { fsyncDirectory(dirname(path)); }
69
+ catch { throw new Error("harness_failure: installation salt directory fsync failed"); }
70
+ return new Uint8Array(salt);
71
+ } catch (error) {
72
+ if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
73
+ return readSaltFile(path);
74
+ }
75
+ } finally {
76
+ if (fd !== undefined) {
77
+ try { closeSync(fd); } catch { /* best-effort close before cleanup */ }
78
+ }
79
+ removeStagingFile(stagingPath);
80
+ }
81
+ }
@@ -0,0 +1,73 @@
1
+ import { localFingerprint } from "../digest";
2
+ import type { RouteDependencyV1, RouteSubjectV1 } from "../events/types";
3
+ import type { LabDestinationV1, LabRouteContext } from "../live/types";
4
+ import { buildBehaviorFingerprintV1 } from "./behavior-fingerprint";
5
+ import { readInstallationSalt } from "./installation-salt";
6
+
7
+ function compareDependency(a: RouteDependencyV1, b: RouteDependencyV1): number {
8
+ const fields: Array<keyof RouteDependencyV1> = [
9
+ "role", "providerId", "providerInstanceFingerprint", "upstreamModelId", "endpointFingerprint",
10
+ "clientModelId", "effectiveAdapter", "upstreamProtocol", "behaviorFingerprint",
11
+ ];
12
+ for (const field of fields) {
13
+ const av = a[field];
14
+ const bv = b[field];
15
+ if (av < bv) return -1;
16
+ if (av > bv) return 1;
17
+ }
18
+ return 0;
19
+ }
20
+
21
+ function canonicalDependencies(dependencies: RouteDependencyV1[] | undefined): RouteDependencyV1[] {
22
+ const rows = (dependencies ?? []).map((row) => Object.freeze({ ...row }));
23
+ rows.sort(compareDependency);
24
+ for (let i = 1; i < rows.length; i++) {
25
+ if (compareDependency(rows[i - 1]!, rows[i]!) === 0) {
26
+ throw new Error("harness_failure: duplicate route dependency");
27
+ }
28
+ }
29
+ return rows;
30
+ }
31
+
32
+ /** Build RouteSubjectV1 only from an approved immutable destination and authoritative effective resolver inputs. */
33
+ export function buildRouteSubjectV1(
34
+ routeContext: LabRouteContext,
35
+ destination: LabDestinationV1,
36
+ configDir?: string,
37
+ ): RouteSubjectV1 {
38
+ if (!routeContext.providerInstanceKey) throw new Error("harness_failure: provider instance identity is required");
39
+ if (!/^[0-9a-f]{64}$/.test(routeContext.opencodexCompatibilityVersion)) {
40
+ throw new Error("harness_failure: invalid opencodexCompatibilityVersion");
41
+ }
42
+ const adapterValue = routeContext.behaviorValues["wire.adapter"]?.value;
43
+ const protocolValue = routeContext.behaviorValues["wire.upstreamProtocol"]?.value;
44
+ if (adapterValue !== routeContext.effectiveAdapter || protocolValue !== routeContext.upstreamProtocol) {
45
+ throw new Error("harness_failure: behavior resolver output does not match exact route");
46
+ }
47
+ const salt = readInstallationSalt(configDir);
48
+ const providerInstanceFingerprint = localFingerprint("providerInstance", routeContext.providerInstanceKey, salt);
49
+ const dependencies = canonicalDependencies(routeContext.dependencies);
50
+ const subject: RouteSubjectV1 = {
51
+ subjectSchemaVersion: 1,
52
+ subjectKind: "route",
53
+ providerId: routeContext.providerId,
54
+ providerInstanceFingerprint,
55
+ clientModelId: routeContext.clientModelId,
56
+ upstreamModelId: routeContext.upstreamModelId,
57
+ effectiveAdapter: routeContext.effectiveAdapter,
58
+ inboundProtocol: routeContext.inboundProtocol,
59
+ upstreamProtocol: routeContext.upstreamProtocol,
60
+ surface: routeContext.surface,
61
+ opencodexCompatibilityVersion: routeContext.opencodexCompatibilityVersion,
62
+ behaviorFingerprint: buildBehaviorFingerprintV1(routeContext.behaviorValues),
63
+ endpointFingerprint: destination.fingerprint,
64
+ dependencies,
65
+ };
66
+ return freezeRouteSubject(subject);
67
+ }
68
+
69
+ /** Deep-freeze route dependencies as well as the subject shell. */
70
+ export function freezeRouteSubject(subject: RouteSubjectV1): RouteSubjectV1 {
71
+ const dependencies = subject.dependencies.map((row) => Object.freeze({ ...row }));
72
+ return Object.freeze({ ...subject, dependencies: Object.freeze(dependencies) as unknown as RouteDependencyV1[] });
73
+ }
@@ -44,6 +44,26 @@ export interface BoundedBodyResult {
44
44
  displaySafe: boolean;
45
45
  }
46
46
 
47
+ export interface BoundedBytesOptions {
48
+ /** Abort the read with this signal. Its reason is rethrown by identity. */
49
+ signal?: AbortSignal;
50
+ /** Maximum number of raw bytes retained from the response body. */
51
+ maxBytes: number;
52
+ }
53
+
54
+ export interface BoundedBytesResult {
55
+ /**
56
+ * Exact raw bytes retained from the response. Empty when the cap was exceeded.
57
+ *
58
+ * This is a view over internal storage, so `bytes.buffer.byteLength` can exceed
59
+ * `bytes.byteLength`. Consumers must honor the view's byteOffset and byteLength
60
+ * instead of reading or transferring the backing buffer directly.
61
+ */
62
+ bytes: Uint8Array<ArrayBuffer>;
63
+ /** True when the body was observed to exceed the byte cap. */
64
+ oversized: boolean;
65
+ }
66
+
47
67
  const TOTAL_TIMEOUT = Symbol("bounded body total timeout");
48
68
  const INACTIVITY_TIMEOUT = Symbol("bounded body inactivity timeout");
49
69
 
@@ -83,6 +103,90 @@ function cancelWithoutWaiting(reader: ReadableStreamDefaultReader<Uint8Array>, r
83
103
  }
84
104
  }
85
105
 
106
+ /**
107
+ * Consume the original response body as raw bytes under a strict memory ceiling.
108
+ *
109
+ * The caller owns any wall-clock deadline through `signal`, which lets one budget
110
+ * cover both response headers and body consumption. No decoding, cloning, or teeing
111
+ * occurs, so arbitrary upstream bytes remain unchanged.
112
+ */
113
+ export async function readBoundedResponseBytes(
114
+ response: Response,
115
+ options: BoundedBytesOptions,
116
+ ): Promise<BoundedBytesResult> {
117
+ const signal = options.signal;
118
+ if (signal?.aborted) throw signal.reason;
119
+
120
+ const body = response.body;
121
+ if (!body) return { bytes: new Uint8Array(0), oversized: false };
122
+
123
+ const reader = body.getReader();
124
+ const maxBytes = options.maxBytes;
125
+ let retained = new Uint8Array(Math.min(maxBytes, 64 * 1024));
126
+ let retainedBytes = 0;
127
+ let mustCancel = false;
128
+ let cancelReason: unknown;
129
+
130
+ let rejectForAbort: ((reason: unknown) => void) | undefined;
131
+ const aborted = new Promise<never>((_resolve, reject) => {
132
+ rejectForAbort = reject;
133
+ });
134
+ const onAbort = () => rejectForAbort?.(signal?.reason);
135
+ signal?.addEventListener("abort", onAbort, { once: true });
136
+ // Close the narrow race between the preflight check and listener install.
137
+ if (signal?.aborted) onAbort();
138
+
139
+ try {
140
+ while (true) {
141
+ const read = reader.read();
142
+ // Observe a late read rejection when abort/cancellation wins the race.
143
+ void read.catch(() => undefined);
144
+ const outcome = await Promise.race([read, aborted]);
145
+ if (signal?.aborted) {
146
+ mustCancel = true;
147
+ cancelReason = signal.reason;
148
+ throw signal.reason;
149
+ }
150
+
151
+ const { value, done } = outcome;
152
+ if (done) {
153
+ return { bytes: retained.subarray(0, retainedBytes), oversized: false };
154
+ }
155
+ if (!value || value.byteLength === 0) continue;
156
+
157
+ if (value.byteLength > maxBytes - retainedBytes) {
158
+ mustCancel = true;
159
+ cancelReason = new DOMException("Response body size limit reached", "QuotaExceededError");
160
+ retained = new Uint8Array(0);
161
+ retainedBytes = 0;
162
+ return { bytes: retained, oversized: true };
163
+ }
164
+
165
+ if (retainedBytes + value.byteLength > retained.length) {
166
+ const grown = new Uint8Array(
167
+ Math.min(maxBytes, Math.max(retained.length * 2, retainedBytes + value.byteLength)),
168
+ );
169
+ grown.set(retained.subarray(0, retainedBytes));
170
+ retained = grown;
171
+ }
172
+ retained.set(value, retainedBytes);
173
+ retainedBytes += value.byteLength;
174
+ }
175
+ } catch (error) {
176
+ mustCancel = true;
177
+ cancelReason = error;
178
+ throw error;
179
+ } finally {
180
+ signal?.removeEventListener("abort", onAbort);
181
+ if (mustCancel) cancelWithoutWaiting(reader, cancelReason);
182
+ try {
183
+ reader.releaseLock();
184
+ } catch {
185
+ // A pending read can keep the lock briefly while cancel settles.
186
+ }
187
+ }
188
+ }
189
+
86
190
  function decodeUtf8(chunks: readonly Uint8Array[], fatal: boolean): string {
87
191
  const decoder = new TextDecoder("utf-8", { fatal });
88
192
  let text = "";
@@ -0,0 +1,13 @@
1
+ import type { TrustedLabRouteExecutor } from "../lab/live/types";
2
+ import { isHostIssuedLabRouteExecutor } from "./lab-live-host";
3
+
4
+ /**
5
+ * Read-only trust check used by the Lab executor.
6
+ *
7
+ * Capability issuance intentionally does not live in this authority module: callers cannot
8
+ * self-attest sandbox boundary names here. Only the dedicated trusted host integration may
9
+ * construct and register an evidence-eligible route executor.
10
+ */
11
+ export function isTrustedLabRouteExecutor(value: unknown): value is TrustedLabRouteExecutor {
12
+ return isHostIssuedLabRouteExecutor(value);
13
+ }
@@ -0,0 +1,30 @@
1
+ import type { LabRouteExecutor, TrustedLabRouteExecutor } from "../lab/live/types";
2
+ import { REQUIRED_LAB_SANDBOX_BOUNDARIES } from "../lab/live/types";
3
+
4
+ const HOST_ISSUED_EXECUTORS = new WeakSet<object>();
5
+
6
+ /**
7
+ * Trusted host integration boundary for CL-03 exact-route execution.
8
+ *
9
+ * This module is intentionally not re-exported from the Lab public surface. The host owns
10
+ * enforcement of every mandatory sandbox boundary and therefore supplies no caller-controlled
11
+ * boundary list. Adapters/providers receive only the resulting opaque capability.
12
+ *
13
+ * @internal host integration only
14
+ */
15
+ export function createHostIssuedLabRouteExecutor(execute: LabRouteExecutor): TrustedLabRouteExecutor {
16
+ const capability: TrustedLabRouteExecutor = Object.freeze({
17
+ execute,
18
+ enforcedBoundaries: Object.freeze([...REQUIRED_LAB_SANDBOX_BOUNDARIES]),
19
+ });
20
+ HOST_ISSUED_EXECUTORS.add(capability);
21
+ return capability;
22
+ }
23
+
24
+ /** Internal recognition check consumed by the read-only authority facade. */
25
+ export function isHostIssuedLabRouteExecutor(value: unknown): value is TrustedLabRouteExecutor {
26
+ if (typeof value !== "object" || value === null || !HOST_ISSUED_EXECUTORS.has(value as object)) return false;
27
+ const capability = value as TrustedLabRouteExecutor;
28
+ return capability.enforcedBoundaries.length === REQUIRED_LAB_SANDBOX_BOUNDARIES.length
29
+ && REQUIRED_LAB_SANDBOX_BOUNDARIES.every((boundary, index) => capability.enforcedBoundaries[index] === boundary);
30
+ }
@@ -0,0 +1,45 @@
1
+ import { PinnedHttpError, pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
2
+ import { TransportError } from "../lab/live/transport";
3
+ import type { LabCredentialLeaseV1, LabPinnedSender } from "../lab/live/types";
4
+
5
+ /** Only response metadata required by current live assertions crosses into Lab. */
6
+ const LAB_RESPONSE_HEADER_ALLOWLIST = ["content-type"] as const;
7
+
8
+ /**
9
+ * Trusted credential/transport owner. Secret headers exist only in this non-Lab module and are
10
+ * consumed directly by the pinned HTTP primitive; they are never returned to Lab code.
11
+ */
12
+ export function createLabAuthorizedPinnedSender(
13
+ authorize: (lease: LabCredentialLeaseV1) => Promise<HeadersInit> | HeadersInit,
14
+ ): LabPinnedSender {
15
+ return async (lease, destination, pinned, request, signal, limits) => {
16
+ const headers = await authorize(lease);
17
+ const url = `${destination.scheme}://${destination.host}:${destination.port}${destination.basePath}${request.path}`;
18
+ const options = {
19
+ headers,
20
+ maxBytes: limits.maxOutputBytes,
21
+ connectTimeoutMs: limits.connectTimeoutMs,
22
+ idleTimeoutMs: Math.min(limits.firstByteTimeoutMs, limits.inactivityTimeoutMs),
23
+ rejectUnauthorized: true,
24
+ context: "Lab provider response",
25
+ };
26
+ let response: Response;
27
+ try {
28
+ response = request.method === "POST"
29
+ ? await pinnedHttpPost(url, pinned, request.body ?? "", signal, options)
30
+ : await pinnedHttpGet(url, pinned, signal, options);
31
+ } catch (error) {
32
+ if (error instanceof PinnedHttpError && error.code === "connect_timeout") {
33
+ throw new TransportError("connect_timeout", "pinned provider connection timed out");
34
+ }
35
+ throw error;
36
+ }
37
+ const body = await response.text();
38
+ const responseHeaders: Record<string, string> = {};
39
+ for (const headerName of LAB_RESPONSE_HEADER_ALLOWLIST) {
40
+ const value = response.headers.get(headerName);
41
+ if (value !== null) responseHeaders[headerName] = value;
42
+ }
43
+ return { status: response.status, headers: responseHeaders, body };
44
+ };
45
+ }
@@ -1,11 +1,20 @@
1
- import http, { type IncomingMessage, type RequestOptions } from "node:http";
1
+ import http, { type ClientRequest, type IncomingMessage, type RequestOptions } from "node:http";
2
2
  import https from "node:https";
3
3
 
4
4
  export type PinnedAddress = { address: string; family: number };
5
5
 
6
+ export type PinnedHttpErrorCode = "connect_timeout";
7
+
8
+ export class PinnedHttpError extends Error {
9
+ override readonly name = "PinnedHttpError";
10
+ constructor(readonly code: PinnedHttpErrorCode, message: string) { super(message); }
11
+ }
12
+
6
13
  export interface PinnedHttpRequestOptions {
7
14
  headers?: HeadersInit;
8
15
  maxBytes?: number;
16
+ /** Optional deadline for establishing the TCP connection and, for HTTPS, completing TLS. */
17
+ connectTimeoutMs?: number;
9
18
  idleTimeoutMs?: number;
10
19
  rejectUnauthorized?: boolean;
11
20
  context?: string;
@@ -27,6 +36,7 @@ function pinnedHttpRequest(
27
36
  throw new Error(`${options?.context ?? "request"} must use HTTP or HTTPS, got ${parsed.protocol}`);
28
37
  }
29
38
  const context = options?.context ?? "request";
39
+ const connectTimeoutMs = options?.connectTimeoutMs;
30
40
  const idleTimeoutMs = options?.idleTimeoutMs ?? 60_000;
31
41
  const maxBytes = options?.maxBytes;
32
42
  const headers = new Headers(options?.headers);
@@ -44,8 +54,15 @@ function pinnedHttpRequest(
44
54
  }
45
55
 
46
56
  let settled = false;
57
+ let req: ClientRequest | undefined;
58
+ let connectTimer: ReturnType<typeof setTimeout> | undefined;
59
+ const clearConnectTimer = () => {
60
+ if (connectTimer !== undefined) clearTimeout(connectTimer);
61
+ connectTimer = undefined;
62
+ };
47
63
  const fail = (error: unknown) => {
48
- try { req.destroy(); } catch { /* ignore */ }
64
+ clearConnectTimer();
65
+ try { req?.destroy(); } catch { /* ignore */ }
49
66
  if (settled) return;
50
67
  settled = true;
51
68
  reject(error instanceof Error ? error : new Error(String(error)));
@@ -83,6 +100,7 @@ function pinnedHttpRequest(
83
100
  };
84
101
 
85
102
  const onResponse = (response: IncomingMessage) => {
103
+ clearConnectTimer();
86
104
  const status = response.statusCode ?? 0;
87
105
  const responseHeaders = new Headers();
88
106
  for (const [key, value] of Object.entries(response.headers)) {
@@ -96,7 +114,7 @@ function pinnedHttpRequest(
96
114
 
97
115
  if (status < 200 || status >= 300) {
98
116
  try { response.destroy(); } catch { /* ignore */ }
99
- try { req.destroy(); } catch { /* ignore */ }
117
+ try { req?.destroy(); } catch { /* ignore */ }
100
118
  if (settled) return;
101
119
  settled = true;
102
120
  resolve(new Response(null, { status, headers: responseHeaders }));
@@ -131,7 +149,7 @@ function pinnedHttpRequest(
131
149
  });
132
150
  },
133
151
  cancel() {
134
- req.destroy();
152
+ req?.destroy();
135
153
  },
136
154
  });
137
155
 
@@ -141,15 +159,26 @@ function pinnedHttpRequest(
141
159
  };
142
160
 
143
161
  const requestFn = parsed.protocol === "https:" ? https.request : http.request;
144
- const req = requestFn(requestOptions, onResponse);
162
+ req = requestFn(requestOptions, onResponse);
145
163
  const onAbort = () => fail(signal?.reason instanceof Error ? signal.reason : new Error("aborted"));
146
164
  signal?.addEventListener("abort", onAbort, { once: true });
165
+ req.on("socket", (socket) => {
166
+ if (!socket.connecting || connectTimeoutMs === undefined) return;
167
+ const connectedEvent = parsed.protocol === "https:" ? "secureConnect" : "connect";
168
+ connectTimer = setTimeout(() => fail(new PinnedHttpError("connect_timeout", `${context} connect timed out`)), connectTimeoutMs);
169
+ socket.once(connectedEvent, clearConnectTimer);
170
+ socket.once("error", clearConnectTimer);
171
+ socket.once("close", clearConnectTimer);
172
+ });
147
173
  req.setTimeout(idleTimeoutMs, () => fail(new Error(`${context} timed out`)));
148
174
  req.on("error", error => {
149
175
  signal?.removeEventListener("abort", onAbort);
150
176
  fail(error);
151
177
  });
152
- req.on("close", () => signal?.removeEventListener("abort", onAbort));
178
+ req.on("close", () => {
179
+ clearConnectTimer();
180
+ signal?.removeEventListener("abort", onAbort);
181
+ });
153
182
  req.end(body);
154
183
  });
155
184
  }
@@ -179,4 +208,4 @@ export function pinnedHttpPost(
179
208
  options?: PinnedHttpRequestOptions,
180
209
  ): Promise<Response> {
181
210
  return pinnedHttpRequest(url, pinned, "POST", body, signal, options);
182
- }
211
+ }
@@ -740,6 +740,11 @@ const OAUTH_RECONCILE_FIELDS: (keyof OcxProviderConfig)[] = [
740
740
  "autoToolChoiceOnlyModels",
741
741
  "preserveReasoningContentModels",
742
742
  ];
743
+ // `requiresReasoningPlaceholderModels` is deliberately NOT reconciled here: no
744
+ // OAuth preset seeds it, so the delete-when-preset-undefined branch would wipe
745
+ // an explicit user opt-out (`[]`) on every startup. Registry seeds still reach
746
+ // existing rows through enrichProviderFromRegistry, which is fill-only and
747
+ // preserves explicit saved values.
743
748
 
744
749
  const GOOGLE_ANTIGRAVITY_PROVIDER = "google-antigravity";
745
750
  const GOOGLE_ANTIGRAVITY_LIVE_DISCOVERY_VERSION = 2 as const;
@@ -883,6 +888,12 @@ export function upsertOAuthProvider(config: OcxConfig, provider: string): void {
883
888
  if (existing?.commandCodeVersion !== undefined) {
884
889
  next.commandCodeVersion = existing.commandCodeVersion;
885
890
  }
891
+ // User-configured price overlays are operator data, not preset state; a
892
+ // re-login, add-account, or reauth must not silently drop them from the
893
+ // Logs/Usage estimates.
894
+ if (existing?.modelCosts !== undefined) {
895
+ next.modelCosts = existing.modelCosts;
896
+ }
886
897
  if (existing && getProviderRegistryEntry(provider)?.allowKeyAuthOverride === true) {
887
898
  // Shared sanitizeApiKeyValue trim / no-CRLF checks from api-key pool writes.
888
899
  let storedApiKey = sanitizeApiKeyValue(existing.apiKey);
@@ -1174,8 +1185,16 @@ export function getLoginStatus(provider: string): { loggedIn: boolean; email?: s
1174
1185
  ...(a.needsReauth ? { needsReauth: true } : {}),
1175
1186
  expiresAt: a.credential.expires,
1176
1187
  }));
1188
+
1189
+ // A stored credential counts as "logged in" when it exists and is not marked for
1190
+ // re-authentication. An expired access token with a valid refresh token is still
1191
+ // logged in: request resolution refreshes expired/near-expiry credentials lazily.
1192
+ // Invalid/unknown local-import expiries are handled at parse/adoption time
1193
+ // (local-token-detect.ts), never by over-reporting login state here.
1194
+ const activeNeedsReauth = set?.accounts
1195
+ .find(a => a.id === set.activeAccountId)?.needsReauth === true;
1177
1196
  return {
1178
- loggedIn: !!cred,
1197
+ loggedIn: !!cred && !activeNeedsReauth,
1179
1198
  email: maskEmail(cred?.email) ?? undefined,
1180
1199
  source: cred?.source,
1181
1200
  error: st?.error,
@@ -24,7 +24,10 @@ export function detectGrokCliToken(): OAuthCredentials | null {
24
24
 
25
25
  const accessToken = entry.key as string;
26
26
  const refreshToken = entry.refresh_token as string;
27
- const expiresAt = entry.expires_at ? new Date(entry.expires_at as string).getTime() : 0;
27
+ const parsedExpiresAt = entry.expires_at ? new Date(entry.expires_at as string).getTime() : 0;
28
+ // Guard against unparseable/NaN expiries: a non-finite value must never be treated as
29
+ // "valid forever". Unknown → 0, which forces the refresh-validation path downstream.
30
+ const expiresAt = Number.isFinite(parsedExpiresAt) ? parsedExpiresAt : 0;
28
31
 
29
32
  return {
30
33
  refresh: refreshToken,
@@ -55,6 +58,9 @@ export function shouldAdoptGrokGeneration(
55
58
  now = Date.now(),
56
59
  refreshSkewMs = 60_000,
57
60
  ): boolean {
61
+ // A non-finite disk expiry means we cannot reason about the generation: the credential is
62
+ // either garbage or unknown. Treat it as requiring refresh validation, never as an upgrade.
63
+ if (!Number.isFinite(disk.expires)) return false;
58
64
  if (disk.expires <= now + refreshSkewMs) return false;
59
65
  const bothExpiriesExist = stored.expires > 0 && disk.expires > 0;
60
66
  if (bothExpiriesExist) return disk.expires >= stored.expires;
@@ -108,7 +114,10 @@ export function parseClaudeOauthPayload(raw: string): OAuthCredentials | null {
108
114
  const data = JSON.parse(raw) as { claudeAiOauth?: { accessToken?: string; refreshToken?: string; expiresAt?: number } };
109
115
  const o = data.claudeAiOauth;
110
116
  if (!o?.accessToken || !o?.refreshToken) return null;
111
- return { access: o.accessToken, refresh: o.refreshToken, expires: o.expiresAt ?? 0, source: "local-cli" };
117
+ // Number.isFinite guard: a string/NaN expiresAt must not flow into downstream time
118
+ // comparisons as a "valid forever" value. Unknown → 0 (refresh-validation path).
119
+ const expires = typeof o.expiresAt === "number" && Number.isFinite(o.expiresAt) ? o.expiresAt : 0;
120
+ return { access: o.accessToken, refresh: o.refreshToken, expires, source: "local-cli" };
112
121
  } catch {
113
122
  return null;
114
123
  }