@bitkyc08/opencodex 2.55.0-preview.20260914 → 2.56.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 (167) hide show
  1. package/gui/dist/assets/{index-DH2PUHqr.js → index-D4zuyIxQ.js} +1 -1
  2. package/gui/dist/index.html +1 -1
  3. package/package.json +2 -1
  4. package/src/adapters/base.ts +21 -0
  5. package/src/adapters/cursor/transport-retry.ts +46 -1
  6. package/src/adapters/cursor.ts +4 -0
  7. package/src/adapters/kiro/adapter.ts +42 -1
  8. package/src/adapters/kiro-retry.ts +23 -4
  9. package/src/adapters/openai-chat/errors.ts +116 -0
  10. package/src/adapters/openai-chat/messages.ts +346 -0
  11. package/src/adapters/openai-chat/passthrough.ts +146 -0
  12. package/src/adapters/openai-chat/response-events.ts +117 -0
  13. package/src/adapters/openai-chat/tool-call-validation.ts +200 -0
  14. package/src/adapters/openai-chat/tool-schema.ts +477 -0
  15. package/src/adapters/openai-chat/wire.ts +50 -0
  16. package/src/adapters/openai-chat.ts +33 -1445
  17. package/src/adapters/openai-responses/canonical-forward.ts +202 -0
  18. package/src/adapters/openai-responses/image-gen.ts +406 -0
  19. package/src/adapters/openai-responses/internal.ts +3 -0
  20. package/src/adapters/openai-responses/passthrough.ts +611 -0
  21. package/src/adapters/openai-responses/prompt-cache.ts +83 -0
  22. package/src/adapters/openai-responses/reasoning.ts +220 -0
  23. package/src/adapters/openai-responses/request-strips.ts +185 -0
  24. package/src/adapters/openai-responses/tool-output-recovery.ts +509 -0
  25. package/src/adapters/openai-responses/tool-schema.ts +293 -0
  26. package/src/adapters/openai-responses/web-search.ts +156 -0
  27. package/src/adapters/openai-responses.ts +4 -2625
  28. package/src/bridge/errors.ts +34 -0
  29. package/src/bridge/internal.ts +174 -0
  30. package/src/bridge/response-json.ts +624 -0
  31. package/src/bridge/sse.ts +1444 -0
  32. package/src/bridge.ts +5 -2204
  33. package/src/chat/inbound.ts +12 -1
  34. package/src/codex/account-lifecycle.ts +3 -0
  35. package/src/codex/account-store.ts +71 -9
  36. package/src/codex/auth-api/account-list.ts +507 -0
  37. package/src/codex/auth-api/http.ts +32 -0
  38. package/src/codex/auth-api/login-flow.ts +554 -0
  39. package/src/codex/auth-api/login-state.ts +64 -0
  40. package/src/codex/auth-api/main-account-probe.ts +331 -0
  41. package/src/codex/auth-api/pool-mode-gate.ts +274 -0
  42. package/src/codex/auth-api/pool-quota-probe.ts +512 -0
  43. package/src/codex/auth-api/reset-credit-service.ts +422 -0
  44. package/src/codex/auth-api/routes.ts +425 -0
  45. package/src/codex/auth-api/runtime-config.ts +48 -0
  46. package/src/codex/auth-api.ts +27 -3118
  47. package/src/codex/auth-context.ts +95 -28
  48. package/src/codex/catalog/auto-review.ts +507 -0
  49. package/src/codex/catalog/build-entries.ts +981 -0
  50. package/src/codex/catalog/combo-member.ts +375 -0
  51. package/src/codex/catalog/derive-entry.ts +229 -0
  52. package/src/codex/catalog/effort.ts +0 -1
  53. package/src/codex/catalog/gated-native-warn.ts +63 -0
  54. package/src/codex/catalog/gather-capture.ts +533 -0
  55. package/src/codex/catalog/model-hints.ts +691 -0
  56. package/src/codex/catalog/model-visibility.ts +304 -0
  57. package/src/codex/catalog/provider-fetch.ts +52 -2942
  58. package/src/codex/catalog/provider-models.ts +685 -0
  59. package/src/codex/catalog/restore.ts +132 -0
  60. package/src/codex/catalog/retained-sync.ts +706 -0
  61. package/src/codex/catalog/routed-gather.ts +858 -0
  62. package/src/codex/catalog/subagent-roster.ts +176 -0
  63. package/src/codex/catalog/sync.ts +52 -2698
  64. package/src/codex/inject/config-toml.ts +563 -0
  65. package/src/codex/inject/remove.ts +192 -0
  66. package/src/codex/inject/restore.ts +540 -0
  67. package/src/codex/inject/routing-classify.ts +109 -0
  68. package/src/codex/inject/routing-target.ts +125 -0
  69. package/src/codex/inject.ts +81 -1436
  70. package/src/codex/lineage.ts +458 -0
  71. package/src/codex/pool-refresh-backoff.ts +152 -0
  72. package/src/codex/routing/active-account.ts +194 -0
  73. package/src/codex/routing/cooldown-math.ts +275 -0
  74. package/src/codex/routing/health-store.ts +402 -0
  75. package/src/codex/routing/probe-lease.ts +358 -0
  76. package/src/codex/routing/selection.ts +703 -0
  77. package/src/codex/routing/thread-affinity.ts +538 -0
  78. package/src/codex/routing.ts +353 -2234
  79. package/src/codex/shim-fingerprint.ts +223 -0
  80. package/src/codex/shim-inspect.ts +175 -0
  81. package/src/codex/shim-probe.ts +367 -0
  82. package/src/codex/shim-restore-lock.ts +169 -0
  83. package/src/codex/shim-state-file.ts +151 -0
  84. package/src/codex/shim-templates.ts +265 -0
  85. package/src/codex/shim.ts +48 -1268
  86. package/src/config/diagnostics.ts +705 -0
  87. package/src/config/feature-flags.ts +55 -0
  88. package/src/config/live-reconcile.ts +403 -0
  89. package/src/config/load-degrade.ts +880 -0
  90. package/src/config/mutation-lock.ts +244 -0
  91. package/src/config/openai-tier-backup.ts +268 -0
  92. package/src/config/persist-unlocked.ts +92 -0
  93. package/src/config/proxy-env.ts +188 -0
  94. package/src/config/salvage.ts +244 -0
  95. package/src/config/schema/config-schema.ts +640 -0
  96. package/src/config/schema/leaf-validators.ts +855 -0
  97. package/src/config/warn-memo.ts +28 -0
  98. package/src/config.ts +234 -4481
  99. package/src/generated/compatibility-version.json +539 -39
  100. package/src/lib/request-execution-budget.ts +69 -20
  101. package/src/lib/spend-reservation-ledger.ts +940 -0
  102. package/src/lib/upstream-retry.ts +55 -11
  103. package/src/lib/workflow-budget.ts +553 -30
  104. package/src/providers/quota/account-cache.ts +441 -0
  105. package/src/providers/quota/antigravity.ts +295 -0
  106. package/src/providers/quota/report-cache.ts +320 -0
  107. package/src/providers/quota/vendor-probes-key.ts +1243 -0
  108. package/src/providers/quota/vendor-probes-oauth.ts +590 -0
  109. package/src/providers/quota.ts +324 -3079
  110. package/src/providers/registry/entries-core.ts +1221 -0
  111. package/src/providers/registry/entries-extended.ts +1204 -0
  112. package/src/providers/registry/model-seeds.ts +908 -0
  113. package/src/providers/registry/types.ts +352 -0
  114. package/src/providers/registry.ts +24 -3536
  115. package/src/responses/continuation-ownership.ts +29 -0
  116. package/src/responses/state/replay-fingerprint.ts +80 -0
  117. package/src/responses/state/snapshot-codec.ts +104 -0
  118. package/src/responses/state/spill-failure.ts +118 -0
  119. package/src/responses/state/spill-queue.ts +665 -0
  120. package/src/responses/state/temp-recovery.ts +257 -0
  121. package/src/responses/state.ts +82 -1143
  122. package/src/routing/identity-domains.ts +449 -0
  123. package/src/routing/probe-lease.ts +511 -0
  124. package/src/server/index/bounded-request.ts +88 -0
  125. package/src/server/index/live-sideband.ts +565 -0
  126. package/src/server/index/serve-options.ts +1766 -0
  127. package/src/server/index/startup-warnings.ts +213 -0
  128. package/src/server/index/websocket-handler.ts +335 -0
  129. package/src/server/index.ts +40 -2547
  130. package/src/server/management/route-registry.ts +26 -23
  131. package/src/server/management/shared.ts +8 -5
  132. package/src/server/management/workflow-budget-routes.ts +133 -0
  133. package/src/server/management-api.ts +12 -0
  134. package/src/server/request-log-conversation.ts +9 -7
  135. package/src/server/request-log.ts +245 -1
  136. package/src/server/responses/account-change-state.ts +233 -0
  137. package/src/server/responses/adapter-continuation.ts +514 -0
  138. package/src/server/responses/adapter-delivery.ts +214 -0
  139. package/src/server/responses/adapter-dispatch.ts +971 -0
  140. package/src/server/responses/compact.ts +59 -4
  141. package/src/server/responses/completion-policy.ts +33 -0
  142. package/src/server/responses/core-auth.ts +527 -0
  143. package/src/server/responses/core-codex-account.ts +859 -0
  144. package/src/server/responses/core-combo-failure.ts +210 -0
  145. package/src/server/responses/core-combo.ts +707 -0
  146. package/src/server/responses/core-errors.ts +152 -0
  147. package/src/server/responses/core-lifetime.ts +95 -0
  148. package/src/server/responses/core-normalize.ts +350 -0
  149. package/src/server/responses/core-opaque-recovery.ts +380 -0
  150. package/src/server/responses/core-options.ts +159 -0
  151. package/src/server/responses/core-replay.ts +225 -0
  152. package/src/server/responses/core.ts +192 -8893
  153. package/src/server/responses/passthrough-delivery.ts +856 -0
  154. package/src/server/responses/passthrough-dispatch.ts +1476 -0
  155. package/src/server/responses/passthrough-execution.ts +54 -0
  156. package/src/server/responses/request-prepare.ts +970 -0
  157. package/src/server/responses/request-send-budget.ts +164 -0
  158. package/src/server/responses/request-sidecar-auth.ts +149 -0
  159. package/src/server/responses/request-transport.ts +744 -0
  160. package/src/server/responses/response-effects.ts +157 -0
  161. package/src/server/responses/run-turn-execution.ts +448 -0
  162. package/src/server/responses/sidecar-execution.ts +469 -0
  163. package/src/server/responses-image-gen-repair.ts +1 -1
  164. package/src/server/workflow-refusal.ts +84 -0
  165. package/src/types/config.ts +30 -0
  166. package/src/usage/log.ts +146 -0
  167. package/src/usage/summary.ts +171 -21
@@ -0,0 +1,705 @@
1
+ import { createHash } from "node:crypto";
2
+ import { lstatSync, readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import * as z from "zod/v4";
5
+ import type { OcxConfig } from "../types";
6
+ import { configReasoningPinsConfigError } from "./provider-validation";
7
+ import { loopbackCompanionAllowed } from "../codex/loopback-target";
8
+ import { UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD } from "../codex/upstream-host-health";
9
+ import { MAX_APP_OWNED_MEMORY_BUDGET_MB, MIN_APP_OWNED_MEMORY_BUDGET_MB } from "../lib/app-owned-memory";
10
+ import { isMissingPathError } from "./atomic-write";
11
+ import { getConfigPath } from "./paths";
12
+ import { getDefaultConfig } from "./proxy-env";
13
+ import { salvageConfigCandidate } from "./salvage";
14
+ import {
15
+ sanitizeReasoningPinsForLoad,
16
+ sanitizeRetryOn429ForLoad,
17
+ sanitizeCapabilityDeclarationsForLoad,
18
+ sanitizeModelCostsForLoad,
19
+ sanitizeAutoReviewForLoad,
20
+ degradedListenerWarnings,
21
+ degradedCodexAccountPriorityWarnings,
22
+ degradedCodexQuotaAutoRefreshWarning,
23
+ normalizeApiKeyIds,
24
+ CLAUDE_SUBAGENT_EFFORTS,
25
+ isClaudeSubagentEffort,
26
+ rawClaudeSubagentEffort,
27
+ normalizeClaudeSubagentEffort,
28
+ malformedUpstreamHostCircuitThresholdWarning,
29
+ malformedPlaintextV2AgentMessagesWarning,
30
+ malformedAgentTaskRecoveryWarning,
31
+ malformedRuntimeRoleWarning,
32
+ malformedOptionalRemoteBlockWarning,
33
+ malformedClientConnectionWarning,
34
+ malformedQuotaResetNotifyWarning,
35
+ malformedCatalogAutoRefreshWarning,
36
+ malformedCodexPoolWarning,
37
+ rawConfigRecord,
38
+ malformedNativeSubagentFields,
39
+ malformedNativeSubagentFieldWarning,
40
+ malformedCodexAccountPickerWarning,
41
+ nativeSubagentSyncDisabledReason,
42
+ normalizeNativeSubagentSync,
43
+ inheritedFastWireConflictProviderNames,
44
+ inheritedFastWireConflictWarning,
45
+ sanitizeModelDisplayNamesForLoad,
46
+ } from "./load-degrade";
47
+ import { configSchema } from "./schema/config-schema";
48
+ import {
49
+ agentTaskRecoverySchema,
50
+ catalogAutoRefreshSchema,
51
+ clientConnectionSchema,
52
+ CODEX_ACCOUNT_PIN_PATTERN,
53
+ codexAccountPrioritiesSchema,
54
+ codexPoolSchema,
55
+ codexQuotaAutoRefreshSchema,
56
+ credentialGroupsSchema,
57
+ hubConfigSchema,
58
+ quotaResetNotifySchema,
59
+ remoteGuiConfigSchema,
60
+ runtimeRoleSchema,
61
+ } from "./schema/leaf-validators";
62
+
63
+ export type ConfigDiagnostics = {
64
+ config: OcxConfig;
65
+ source: "default" | "file" | "fallback";
66
+ error: string | null;
67
+ /** Non-fatal config concerns; absent when there are no warnings. */
68
+ warnings?: string[];
69
+ };
70
+
71
+ export type ConfigFileSnapshot = {
72
+ diagnostics: ConfigDiagnostics;
73
+ /** Exact file contents, including a possible BOM, used as the optimistic revision. */
74
+ raw?: string;
75
+ };
76
+
77
+ function configPlaceholderWarnings(config: OcxConfig): string[] {
78
+ const warnings: string[] = [];
79
+ for (const [name, provider] of Object.entries(config.providers)) {
80
+ const placeholder = provider.baseUrl.match(/\{[^}]*\}/)?.[0];
81
+ if (placeholder) {
82
+ warnings.push(`providers.${name}.baseUrl contains unresolved ${placeholder}; set the real provider URL`);
83
+ }
84
+ }
85
+ return warnings;
86
+ }
87
+
88
+ function validFileConfigDiagnostics(config: OcxConfig, rawParsed: unknown): ConfigDiagnostics {
89
+ // Unsafe hand-edited optional values are disabled in memory instead of rejecting
90
+ // the entire config, which would hide unrelated providers/accounts. The next
91
+ // ordinary save persists the normalized absence.
92
+ const syncDisabledReason = nativeSubagentSyncDisabledReason(config, rawParsed);
93
+ const rawEffort = rawClaudeSubagentEffort(rawParsed);
94
+ const normalized = normalizeClaudeSubagentEffort(normalizeNativeSubagentSync(config, rawParsed), rawParsed);
95
+ const warnings = configPlaceholderWarnings(normalized);
96
+ warnings.push(...inheritedFastWireConflictProviderNames(normalized).map(inheritedFastWireConflictWarning));
97
+ warnings.push(...degradedCodexAccountPriorityWarnings(rawParsed, normalized));
98
+ warnings.push(...degradedListenerWarnings(rawParsed, normalized));
99
+ const quotaAutoRefreshWarning = degradedCodexQuotaAutoRefreshWarning(rawParsed, normalized);
100
+ if (quotaAutoRefreshWarning) warnings.push(quotaAutoRefreshWarning);
101
+ if (rawEffort !== undefined && !isClaudeSubagentEffort(rawEffort)) {
102
+ warnings.push(`claudeCode.subagentEffort ignored: expected one of ${CLAUDE_SUBAGENT_EFFORTS.join(", ")}`);
103
+ }
104
+ warnings.push(...malformedNativeSubagentFields(rawParsed).map(malformedNativeSubagentFieldWarning));
105
+ const pickerWarning = malformedCodexAccountPickerWarning(rawParsed);
106
+ if (pickerWarning) warnings.push(pickerWarning);
107
+ const hostCircuitWarning = malformedUpstreamHostCircuitThresholdWarning(rawParsed);
108
+ if (hostCircuitWarning) warnings.push(hostCircuitWarning);
109
+ const recoveryWarning = malformedAgentTaskRecoveryWarning(rawParsed);
110
+ if (recoveryWarning) warnings.push(recoveryWarning);
111
+ const runtimeRoleWarning = malformedRuntimeRoleWarning(rawParsed);
112
+ if (runtimeRoleWarning) warnings.push(runtimeRoleWarning);
113
+ const hubWarning = malformedOptionalRemoteBlockWarning(rawParsed, "hub");
114
+ if (hubWarning) warnings.push(hubWarning);
115
+ const remoteGuiWarning = malformedOptionalRemoteBlockWarning(rawParsed, "remoteGui");
116
+ if (remoteGuiWarning) warnings.push(remoteGuiWarning);
117
+ const clientWarning = malformedClientConnectionWarning(rawParsed);
118
+ if (clientWarning) warnings.push(clientWarning);
119
+ const notifyWarning = malformedQuotaResetNotifyWarning(rawParsed);
120
+ if (notifyWarning) warnings.push(notifyWarning);
121
+ const catalogRefreshWarning = malformedCatalogAutoRefreshWarning(rawParsed);
122
+ if (catalogRefreshWarning) warnings.push(catalogRefreshWarning);
123
+ const codexPoolWarning = malformedCodexPoolWarning(rawParsed);
124
+ if (codexPoolWarning) warnings.push(codexPoolWarning);
125
+ const plaintextWarning = malformedPlaintextV2AgentMessagesWarning(rawParsed);
126
+ if (plaintextWarning) warnings.push(plaintextWarning);
127
+ if (syncDisabledReason) {
128
+ warnings.push(`syncCodexSubagentDefaults ignored: ${syncDisabledReason}`);
129
+ }
130
+ return {
131
+ config: normalized,
132
+ source: "file",
133
+ error: null,
134
+ ...(warnings.length > 0 ? { warnings } : {}),
135
+ };
136
+ }
137
+
138
+ export function subagentDefaultSyncEffective(
139
+ config: Pick<OcxConfig, "syncCodexSubagentDefaults" | "injectionModel">,
140
+ ): boolean {
141
+ return config.syncCodexSubagentDefaults === true && Boolean(config.injectionModel?.trim());
142
+ }
143
+
144
+ export function mergeConfigDefaults(parsed: unknown): unknown {
145
+ if (!parsed || typeof parsed !== "object") return parsed;
146
+ const defaults = getDefaultConfig();
147
+ const raw = parsed as Record<string, unknown>;
148
+ // Same absence-is-meaningful pin as the repair merge above.
149
+ const merged: Record<string, unknown> = {
150
+ ...defaults,
151
+ ...raw,
152
+ subagentModelsVersion: raw.subagentModelsVersion,
153
+ multiAgentMode: raw.multiAgentMode,
154
+ multiAgentSurfaceAdvisoryVersion: raw.multiAgentSurfaceAdvisoryVersion,
155
+ };
156
+ if (raw.providers && typeof raw.providers === "object" && defaults.providers) {
157
+ merged.providers = { ...defaults.providers, ...(raw.providers as Record<string, unknown>) };
158
+ }
159
+ return merged;
160
+ }
161
+
162
+ function schemaDiagnosticsError(error: z.ZodError): string {
163
+ const details = error.issues.map(issue => {
164
+ const path = issue.path.join(".") || "config";
165
+ return `${path}: ${issue.message}`;
166
+ });
167
+ return details.length > 0 ? `schema_invalid: ${details.join("; ")}` : "schema_invalid";
168
+ }
169
+
170
+ /**
171
+ * Reject a hostname the schema deliberately degrades on read. Load-time has to keep a
172
+ * blank value non-fatal (see the `hostname` field comment), but an incoming write is a
173
+ * live caller who can be told the value is wrong — silently rewriting it to loopback
174
+ * would look like the bind succeeded on the address they asked for.
175
+ */
176
+ function blankHostnameError(value: unknown): string | null {
177
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
178
+ const hostname = (value as Record<string, unknown>).hostname;
179
+ if (hostname === undefined) return null;
180
+ if (typeof hostname !== "string" || !hostname.trim()) {
181
+ return "schema_invalid: hostname: must be a nonblank bind address";
182
+ }
183
+ return null;
184
+ }
185
+
186
+ function claudeSubagentEffortError(value: unknown): string | null {
187
+ const effort = rawClaudeSubagentEffort(value);
188
+ if (effort === undefined || isClaudeSubagentEffort(effort)) return null;
189
+ return `schema_invalid: claudeCode.subagentEffort: must be one of ${CLAUDE_SUBAGENT_EFFORTS.join(", ")}`;
190
+ }
191
+
192
+ function appOwnedMemoryBudgetError(value: unknown): string | null {
193
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
194
+ const budget = (value as Record<string, unknown>).appOwnedMemoryBudgetMb;
195
+ if (budget === undefined) return null;
196
+ if (typeof budget !== "number" || !Number.isInteger(budget)
197
+ || budget < MIN_APP_OWNED_MEMORY_BUDGET_MB || budget > MAX_APP_OWNED_MEMORY_BUDGET_MB) {
198
+ return `schema_invalid: appOwnedMemoryBudgetMb: must be an integer from ${MIN_APP_OWNED_MEMORY_BUDGET_MB} to ${MAX_APP_OWNED_MEMORY_BUDGET_MB}`;
199
+ }
200
+ return null;
201
+ }
202
+
203
+ function upstreamHostCircuitThresholdError(value: unknown): string | null {
204
+ const raw = rawConfigRecord(value);
205
+ if (!raw || !Object.hasOwn(raw, "upstreamHostCircuitThreshold")) return null;
206
+ const threshold = raw.upstreamHostCircuitThreshold;
207
+ if (threshold === undefined) return null;
208
+ if (typeof threshold === "number"
209
+ && Number.isInteger(threshold)
210
+ && threshold >= 0
211
+ && threshold <= UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD) return null;
212
+ return `schema_invalid: upstreamHostCircuitThreshold: must be an integer from 0 to ${UPSTREAM_HOST_CIRCUIT_MAX_THRESHOLD}`;
213
+ }
214
+
215
+ function plaintextV2AgentMessagesError(value: unknown): string | null {
216
+ return malformedPlaintextV2AgentMessagesWarning(value)
217
+ ? "schema_invalid: plaintextV2AgentMessages: must be a boolean or omitted"
218
+ : null;
219
+ }
220
+
221
+ function agentTaskRecoveryError(value: unknown): string | null {
222
+ const raw = rawConfigRecord(value);
223
+ if (!raw || !Object.hasOwn(raw, "agentTaskRecovery") || raw.agentTaskRecovery === undefined) return null;
224
+ const result = agentTaskRecoverySchema.safeParse(raw.agentTaskRecovery);
225
+ if (result.success) return null;
226
+ const issue = result.error.issues[0];
227
+ const field = issue?.path.join(".");
228
+ return `schema_invalid: agentTaskRecovery${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
229
+ }
230
+
231
+ function runtimeRoleError(value: unknown): string | null {
232
+ const raw = rawConfigRecord(value);
233
+ if (!raw || !Object.hasOwn(raw, "runtimeRole") || raw.runtimeRole === undefined) return null;
234
+ if (runtimeRoleSchema.safeParse(raw.runtimeRole).success) return null;
235
+ return 'schema_invalid: runtimeRole: must be one of "standalone", "hub", or "client"';
236
+ }
237
+
238
+ function remoteGuiConfigError(value: unknown): string | null {
239
+ const raw = rawConfigRecord(value);
240
+ if (!raw) return null;
241
+ for (const [key, schema] of [
242
+ ["hub", hubConfigSchema],
243
+ ["remoteGui", remoteGuiConfigSchema],
244
+ ] as const) {
245
+ if (!Object.hasOwn(raw, key) || raw[key] === undefined) continue;
246
+ const result = schema.safeParse(raw[key]);
247
+ if (result.success) continue;
248
+ const issue = result.error.issues[0];
249
+ const field = issue?.path.join(".");
250
+ return `schema_invalid: ${key}${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
251
+ }
252
+ return null;
253
+ }
254
+
255
+ function clientConnectionConfigError(value: unknown): string | null {
256
+ const raw = rawConfigRecord(value);
257
+ if (!raw || !Object.hasOwn(raw, "client") || raw.client === undefined) return null;
258
+ const result = clientConnectionSchema.safeParse(raw.client);
259
+ if (result.success) return null;
260
+ const issue = result.error.issues[0];
261
+ const field = issue?.path.join(".");
262
+ return `schema_invalid: client${field ? `.${field}` : ""}: ${issue?.message ?? "invalid client connection"}`;
263
+ }
264
+
265
+ function clientRolePairError(value: unknown): string | null {
266
+ const raw = rawConfigRecord(value);
267
+ if (!raw) return null;
268
+ const hasClient = Object.hasOwn(raw, "client") && raw.client !== undefined;
269
+ if (raw.runtimeRole === "client" && !hasClient) {
270
+ return "schema_invalid: runtimeRole client requires a complete client connection";
271
+ }
272
+ if (hasClient && raw.runtimeRole !== "client") {
273
+ return "schema_invalid: client connection requires runtimeRole client";
274
+ }
275
+ return null;
276
+ }
277
+
278
+ function quotaResetNotifyError(value: unknown): string | null {
279
+ const raw = rawConfigRecord(value);
280
+ if (!raw || !Object.hasOwn(raw, "quotaResetNotify") || raw.quotaResetNotify === undefined) return null;
281
+ const result = quotaResetNotifySchema.safeParse(raw.quotaResetNotify);
282
+ if (result.success) return null;
283
+ const issue = result.error.issues[0];
284
+ const field = issue?.path.join(".");
285
+ return `schema_invalid: quotaResetNotify${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
286
+ }
287
+
288
+ function catalogAutoRefreshError(value: unknown): string | null {
289
+ const raw = rawConfigRecord(value);
290
+ if (!raw || !Object.hasOwn(raw, "catalogAutoRefresh") || raw.catalogAutoRefresh === undefined) return null;
291
+ const result = catalogAutoRefreshSchema.safeParse(raw.catalogAutoRefresh);
292
+ if (result.success) return null;
293
+ const issue = result.error.issues[0];
294
+ const field = issue?.path.join(".");
295
+ return `schema_invalid: catalogAutoRefresh${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
296
+ }
297
+
298
+ /**
299
+ * The read path degrades a malformed pool policy to undefined, which for an exclusion policy means
300
+ * the excluded accounts quietly keep serving traffic. Reject it on write so `ocx config set` cannot
301
+ * create a policy that looks applied and is not.
302
+ */
303
+ function codexPoolError(value: unknown): string | null {
304
+ const raw = rawConfigRecord(value);
305
+ if (!raw || !Object.hasOwn(raw, "codexPool") || raw.codexPool === undefined) return null;
306
+ const result = codexPoolSchema.safeParse(raw.codexPool);
307
+ if (result.success) return null;
308
+ const issue = result.error.issues[0];
309
+ const field = issue?.path.join(".");
310
+ return `schema_invalid: codexPool${field ? `.${field}` : ""}: ${issue?.message ?? "invalid configuration"}`;
311
+ }
312
+
313
+ /**
314
+ * Same reasoning as {@link blankHostnameError}, and more urgent: the read path degrades a
315
+ * malformed selection-order map to undefined, which on a write would drop every entry the
316
+ * user had accumulated and still report success. A load-time degrade leaves the raw map in
317
+ * the file to be repaired by hand; a degraded write erases it. One bad `ocx config set`
318
+ * must not cost the whole map, so a live caller is told instead.
319
+ */
320
+ function codexAccountPrioritiesError(value: unknown): string | null {
321
+ const raw = rawConfigRecord(value);
322
+ if (!raw) return null;
323
+ if (raw.codexAccountPriorities !== undefined) {
324
+ const parsed = codexAccountPrioritiesSchema.safeParse(raw.codexAccountPriorities);
325
+ if (!parsed.success) {
326
+ return schemaDiagnosticsError(parsed.error).replace("schema_invalid: ", "schema_invalid: codexAccountPriorities.");
327
+ }
328
+ }
329
+ // Tested as a string rather than coerced: `String(123)` matches the id pattern, so a
330
+ // coercing guard waves a non-string pin through to the schema, where `.catch(undefined)`
331
+ // drops it and reports the write as a success — the exact silent-degrade this guards.
332
+ const pin = raw.activeCodexAccountPinned;
333
+ if (pin !== undefined && (typeof pin !== "string" || !CODEX_ACCOUNT_PIN_PATTERN.test(pin))) {
334
+ return "schema_invalid: activeCodexAccountPinned: must be an account id";
335
+ }
336
+ return null;
337
+ }
338
+
339
+ /**
340
+ * Same reasoning as {@link codexAccountPrioritiesError}, plus one of its own. The read
341
+ * path drops an invalid grouping, so a degraded write would erase a declaration the
342
+ * operator is still editing and still report success. And an ambiguous declaration --
343
+ * one id used twice, one credential in two groups -- has no safe silent answer at all:
344
+ * resolving it by list order would quietly merge two quota domains. A live caller is
345
+ * told which group is the problem instead.
346
+ */
347
+ export function poolCredentialGroupsError(value: unknown): string | null {
348
+ const pool = rawConfigRecord(rawConfigRecord(value)?.pool);
349
+ if (!pool || pool.credentialGroups === undefined) return null;
350
+ const parsed = credentialGroupsSchema.safeParse(pool.credentialGroups);
351
+ if (parsed.success) return null;
352
+ const details = parsed.error.issues.map(issue => {
353
+ const path = issue.path.join(".");
354
+ return path ? `${path}: ${issue.message}` : issue.message;
355
+ }).join("; ");
356
+ return `schema_invalid: pool.credentialGroups: ${details}`;
357
+ }
358
+
359
+ function codexQuotaAutoRefreshError(value: unknown): string | null {
360
+ const raw = rawConfigRecord(value);
361
+ if (!raw || raw.codexQuotaAutoRefresh === undefined) return null;
362
+ const parsed = codexQuotaAutoRefreshSchema.safeParse(raw.codexQuotaAutoRefresh);
363
+ if (parsed.success) return null;
364
+ const details = parsed.error.issues.map(issue => {
365
+ const path = issue.path.join(".");
366
+ const message = path === ""
367
+ ? issue.message.replace(/^codexQuotaAutoRefresh\s*/, "")
368
+ : issue.message;
369
+ return `codexQuotaAutoRefresh${path ? `.${path}` : ""}: ${message}`;
370
+ });
371
+ return `schema_invalid: ${details.join("; ")}`;
372
+ }
373
+
374
+ function googleAntigravityStaticCatalogVersionError(value: unknown): string | null {
375
+ const raw = rawConfigRecord(value);
376
+ if (!raw || !Object.hasOwn(raw, "googleAntigravityStaticCatalogVersion")) return null;
377
+ const version = raw.googleAntigravityStaticCatalogVersion;
378
+ if (version === undefined || version === 1 || version === 2) return null;
379
+ return "schema_invalid: googleAntigravityStaticCatalogVersion: must be 1, 2, or omitted";
380
+ }
381
+
382
+ function codexAccountPickerEnabledError(value: unknown): string | null {
383
+ const raw = rawConfigRecord(value);
384
+ if (!raw) return null;
385
+ const descriptor = Object.getOwnPropertyDescriptor(raw, "codexAccountPickerEnabled");
386
+ if (!descriptor) {
387
+ return "codexAccountPickerEnabled" in raw
388
+ ? "schema_invalid: codexAccountPickerEnabled: must be an own boolean data property or omitted"
389
+ : null;
390
+ }
391
+ if (!("value" in descriptor)) {
392
+ return "schema_invalid: codexAccountPickerEnabled: must be an own boolean data property or omitted";
393
+ }
394
+ const enabled = descriptor.value;
395
+ if (enabled === undefined || typeof enabled === "boolean") return null;
396
+ return "schema_invalid: codexAccountPickerEnabled: must be a boolean or omitted";
397
+ }
398
+
399
+ function emptyCompletionRetryError(value: unknown): string | null {
400
+ const raw = rawConfigRecord(value);
401
+ if (!raw || !Object.hasOwn(raw, "emptyCompletionRetry")) return null;
402
+ const enabled = raw.emptyCompletionRetry;
403
+ if (enabled === undefined || typeof enabled === "boolean") return null;
404
+ return "schema_invalid: emptyCompletionRetry: must be a boolean or omitted";
405
+ }
406
+
407
+ function dropCodexSafetyBufferingError(value: unknown): string | null {
408
+ const raw = rawConfigRecord(value);
409
+ if (!raw || !Object.hasOwn(raw, "dropCodexSafetyBuffering")) return null;
410
+ const enabled = raw.dropCodexSafetyBuffering;
411
+ if (enabled === undefined || typeof enabled === "boolean") return null;
412
+ return "schema_invalid: dropCodexSafetyBuffering: must be a boolean or omitted";
413
+ }
414
+
415
+ function oauthOpenBrowserError(value: unknown): string | null {
416
+ const raw = rawConfigRecord(value);
417
+ if (!raw || !Object.hasOwn(raw, "oauthOpenBrowser")) return null;
418
+ const enabled = raw.oauthOpenBrowser;
419
+ if (enabled === undefined || typeof enabled === "boolean") return null;
420
+ return "schema_invalid: oauthOpenBrowser: must be a boolean or omitted";
421
+ }
422
+
423
+ /** Validate an in-memory config candidate without touching disk. Used by headless CLI import/set. */
424
+ /**
425
+ * Reject a loopback-listener port that collides with the proxy port (#1102), and a port-less
426
+ * companion listener on a bind address that already owns 127.0.0.1 (#4236).
427
+ *
428
+ * The schema can only check the shape of each field on its own; the two ports being distinct —
429
+ * and the port-less form being compatible with `hostname` — are relationships between fields.
430
+ * Letting either through would surface as a startup failure after the public listener already
431
+ * bound, which reads like an unrelated port conflict.
432
+ *
433
+ * Both keys are read from the same candidate, so `ocx config set hostname 127.0.0.1` on a host
434
+ * whose listener is already the companion form is refused by this same check, with the same
435
+ * message, rather than breaking the next start.
436
+ *
437
+ * This is write-time only, matching `blankHostnameError`: a live caller can be told the value
438
+ * is wrong, whereas a hand-edited config on the read path degrades to undefined rather than
439
+ * resetting the whole file. `assertLoopbackListenerBindable` repeats the decision at startup so
440
+ * a hand edit that skipped this boundary fails with the same sentence instead of EADDRINUSE.
441
+ */
442
+ function loopbackListenerPortError(value: unknown): string | null {
443
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
444
+ const listener = (value as Record<string, unknown>).unauthenticatedLoopbackListener;
445
+ if (listener === undefined) return null;
446
+ if (!listener || typeof listener !== "object" || Array.isArray(listener)) {
447
+ return "schema_invalid: unauthenticatedLoopbackListener: must be an object or omitted";
448
+ }
449
+ const entry = listener as Record<string, unknown>;
450
+ // `enabled` must be a real boolean. The schema's `.catch(undefined)` would otherwise DELETE
451
+ // a `"true"` string entry and report success, leaving an operator convinced they enabled an
452
+ // unauthenticated listener that is in fact off. Load-time still degrades quietly — a hand
453
+ // edit must not reset the file — but a live caller gets told.
454
+ if (typeof entry.enabled !== "boolean") {
455
+ return "schema_invalid: unauthenticatedLoopbackListener.enabled: must be a boolean";
456
+ }
457
+ if (entry.enabled !== true) return null;
458
+ const hostname = typeof (value as Record<string, unknown>).hostname === "string"
459
+ ? (value as Record<string, unknown>).hostname as string
460
+ : undefined;
461
+ const proxyPort = (value as Record<string, unknown>).port;
462
+ const listenerPort = entry.port;
463
+ // The companion form. `port` omitted means "same port as the public listener, on 127.0.0.1",
464
+ // which only exists as a free address when the public listener is bound somewhere else.
465
+ if (listenerPort === undefined) {
466
+ return loopbackCompanionBindError(
467
+ hostname,
468
+ typeof proxyPort === "number" ? proxyPort : 10100,
469
+ );
470
+ }
471
+ if (typeof listenerPort !== "number" || !Number.isInteger(listenerPort) || listenerPort < 1 || listenerPort > 65535) {
472
+ return "schema_invalid: unauthenticatedLoopbackListener.port: must be an integer port when enabled, or omitted to share the proxy port";
473
+ }
474
+ if (typeof proxyPort === "number" && proxyPort === listenerPort) {
475
+ return "schema_invalid: unauthenticatedLoopbackListener.port: must differ from the proxy port";
476
+ }
477
+ return null;
478
+ }
479
+
480
+ /**
481
+ * The one sentence both the write boundary and startup use for an impossible companion bind.
482
+ *
483
+ * Exported so `startServer` can fail with the identical text: an operator who hand-edited the
484
+ * file past `validateConfigCandidate` must read the same diagnosis, not EADDRINUSE.
485
+ */
486
+ export function loopbackCompanionBindError(
487
+ hostname: string | undefined,
488
+ proxyPort: number,
489
+ ): string | null {
490
+ if (loopbackCompanionAllowed(hostname)) return null;
491
+ const bind = (hostname ?? "").trim() || "127.0.0.1";
492
+ return "schema_invalid: unauthenticatedLoopbackListener: a port-less listener binds "
493
+ + `127.0.0.1:${proxyPort}, which the public listener on hostname "${bind}" already holds. `
494
+ + "Either set a distinct unauthenticatedLoopbackListener.port, or remove the listener — a "
495
+ + "loopback bind already admits local callers without a credential.";
496
+ }
497
+
498
+ /**
499
+ * Validate the hub management ingress at the live-write boundary.
500
+ *
501
+ * The persisted schema intentionally degrades a malformed hand edit to disabled so a typo in
502
+ * this opt-in listener cannot discard providers or credentials. A live config mutation must not
503
+ * get that leniency: it receives an exact field error before the degrading schema is applied.
504
+ */
505
+ function managementIngressConfigError(value: unknown): string | null {
506
+ const raw = rawConfigRecord(value);
507
+ if (!raw) return null;
508
+ const hub = rawConfigRecord(raw.hub);
509
+ if (!hub || !Object.hasOwn(hub, "managementIngress") || hub.managementIngress === undefined) return null;
510
+ const ingress = rawConfigRecord(hub.managementIngress);
511
+ if (!ingress) {
512
+ return "schema_invalid: hub.managementIngress: must be an object or omitted";
513
+ }
514
+ if (typeof ingress.enabled !== "boolean") {
515
+ return "schema_invalid: hub.managementIngress.enabled: must be a boolean";
516
+ }
517
+ const keys = Object.keys(ingress);
518
+ if (ingress.enabled === false) {
519
+ return keys.length === 1
520
+ ? null
521
+ : "schema_invalid: hub.managementIngress: disabled ingress accepts only enabled";
522
+ }
523
+ if (keys.some(key => key !== "enabled" && key !== "port")) {
524
+ return "schema_invalid: hub.managementIngress: contains an unsupported field";
525
+ }
526
+ const ingressPort = ingress.port;
527
+ if (typeof ingressPort !== "number" || !Number.isInteger(ingressPort) || ingressPort < 1 || ingressPort > 65535) {
528
+ return "schema_invalid: hub.managementIngress.port: must be an integer port when enabled";
529
+ }
530
+ if (raw.runtimeRole !== "hub") {
531
+ return "schema_invalid: hub.managementIngress: enabled ingress requires runtimeRole hub";
532
+ }
533
+ const proxyPort = typeof raw.port === "number" ? raw.port : 10100;
534
+ if (proxyPort === ingressPort) {
535
+ return "schema_invalid: hub.managementIngress.port: must differ from the proxy port";
536
+ }
537
+ const loopback = rawConfigRecord(raw.unauthenticatedLoopbackListener);
538
+ if (loopback?.enabled === true && loopback.port === ingressPort) {
539
+ return "schema_invalid: hub.managementIngress.port: must differ from unauthenticatedLoopbackListener.port";
540
+ }
541
+ return null;
542
+ }
543
+
544
+ export function validateConfigCandidate(value: unknown): { ok: true; config: OcxConfig } | { ok: false; error: string } {
545
+ const boundaryError = configReasoningPinsConfigError(value)
546
+ ?? blankHostnameError(value)
547
+ ?? claudeSubagentEffortError(value)
548
+ ?? appOwnedMemoryBudgetError(value)
549
+ ?? upstreamHostCircuitThresholdError(value)
550
+ ?? plaintextV2AgentMessagesError(value)
551
+ ?? agentTaskRecoveryError(value)
552
+ ?? quotaResetNotifyError(value)
553
+ ?? catalogAutoRefreshError(value)
554
+ ?? codexPoolError(value)
555
+ ?? googleAntigravityStaticCatalogVersionError(value)
556
+ ?? codexAccountPrioritiesError(value)
557
+ ?? poolCredentialGroupsError(value)
558
+ ?? codexQuotaAutoRefreshError(value)
559
+ ?? codexAccountPickerEnabledError(value)
560
+ ?? emptyCompletionRetryError(value)
561
+ ?? dropCodexSafetyBufferingError(value)
562
+ ?? oauthOpenBrowserError(value)
563
+ ?? runtimeRoleError(value)
564
+ ?? remoteGuiConfigError(value)
565
+ ?? clientConnectionConfigError(value)
566
+ ?? clientRolePairError(value)
567
+ ?? loopbackListenerPortError(value)
568
+ ?? managementIngressConfigError(value);
569
+ if (boundaryError) return { ok: false, error: boundaryError };
570
+ const result = configSchema.safeParse(value);
571
+ if (result.success) {
572
+ const config = normalizeApiKeyIds(result.data as OcxConfig);
573
+ return { ok: true, config };
574
+ }
575
+ return { ok: false, error: schemaDiagnosticsError(result.error) };
576
+ }
577
+
578
+ export function configDiagnosticsFromRaw(raw: string): ConfigDiagnostics {
579
+ try {
580
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
581
+ sanitizeReasoningPinsForLoad(parsed);
582
+ // Same degradation as loadConfig: a hand-edited invalid retryOn429 must not trip the
583
+ // schema and send the caller a default-config fallback (the config command could then
584
+ // persist that fallback over the user's providers/keys).
585
+ sanitizeModelDisplayNamesForLoad(parsed);
586
+ sanitizeAutoReviewForLoad(parsed);
587
+ sanitizeRetryOn429ForLoad(parsed);
588
+ sanitizeModelCostsForLoad(parsed);
589
+ sanitizeCapabilityDeclarationsForLoad(parsed);
590
+ const result = configSchema.safeParse(parsed);
591
+ if (result.success) {
592
+ return validFileConfigDiagnostics(normalizeApiKeyIds(result.data as OcxConfig), parsed);
593
+ }
594
+
595
+ const merged = mergeConfigDefaults(parsed);
596
+ const retryResult = configSchema.safeParse(merged);
597
+ if (retryResult.success) {
598
+ return validFileConfigDiagnostics(normalizeApiKeyIds(retryResult.data as OcxConfig), parsed);
599
+ }
600
+
601
+ // #1785: one invalid routing profile must not make diagnostics report the built-in
602
+ // defaults AS the config, because a later config write persists those defaults over the
603
+ // operator's providers, keys and prices.
604
+ //
605
+ // The failure is still reported. `source` stays "fallback" and `error` keeps the real
606
+ // schema message -- diagnostics is the surface that tells callers the file is invalid,
607
+ // and every consumer that must refuse an invalid config (provider reload, catalog sync,
608
+ // cost reconcile, codex admission) gates on exactly those two fields. Only `config`
609
+ // changes: it carries the salvaged document instead of factory defaults, so a caller
610
+ // that ignores the error and writes it back preserves what the operator configured.
611
+ const salvaged = salvageConfigCandidate(merged, retryResult.error);
612
+ if (salvaged) {
613
+ const config = normalizeApiKeyIds(salvaged.parsed);
614
+ const warnings = degradedListenerWarnings(parsed, config);
615
+ return {
616
+ config,
617
+ source: "fallback",
618
+ error: schemaDiagnosticsError(result.error),
619
+ ...(warnings.length > 0 ? { warnings } : {}),
620
+ };
621
+ }
622
+
623
+ return { config: getDefaultConfig(), source: "fallback", error: schemaDiagnosticsError(result.error) };
624
+ } catch {
625
+ return { config: getDefaultConfig(), source: "fallback", error: "invalid_json" };
626
+ }
627
+ }
628
+
629
+ export function readConfigFileSnapshot(): ConfigFileSnapshot {
630
+ try {
631
+ const raw = readFileSync(getConfigPath(), "utf-8");
632
+ return { diagnostics: configDiagnosticsFromRaw(raw), raw };
633
+ } catch (error) {
634
+ if (isMissingPathError(error)) {
635
+ return {
636
+ diagnostics: { config: getDefaultConfig(), source: "default", error: null },
637
+ };
638
+ }
639
+ return {
640
+ diagnostics: { config: getDefaultConfig(), source: "fallback", error: "invalid_json" },
641
+ };
642
+ }
643
+ }
644
+
645
+ export function readConfigDiagnostics(): ConfigDiagnostics {
646
+ return readConfigFileSnapshot().diagnostics;
647
+ }
648
+
649
+ /** Read-only init preflight. Occupied unsafe entries are never treated as absence. */
650
+ export function observeInitialConfigState(): "missing" | "exists" | "invalid" {
651
+ try {
652
+ if (!lstatSync(getConfigPath()).isFile()) return "invalid";
653
+ } catch (error) {
654
+ return isMissingPathError(error) ? "missing" : "invalid";
655
+ }
656
+ return readConfigFileSnapshot().diagnostics.source === "file" ? "exists" : "invalid";
657
+ }
658
+
659
+ /**
660
+ * The persisted config, plus a digest of the EXACT bytes it was parsed from.
661
+ *
662
+ * A union rather than a nullable digest, because `{ kind: "read" }` with no
663
+ * digest is a state that cannot occur — and a state that cannot occur should
664
+ * not be a state that can be written down. Refusing it at runtime is a check
665
+ * somebody eventually forgets; making it unrepresentable is not.
666
+ *
667
+ * Why a byte digest at all: the Codex write lock compares an authority snapshot
668
+ * taken before the lock against one taken while holding it, and its config
669
+ * component used to hash the PARSED object. Two files that differ only in
670
+ * whitespace or key order parse identically, so a non-cooperating writer could
671
+ * rewrite the file between admission and commit and the comparison would see
672
+ * nothing. Hashing what was actually read closes that.
673
+ *
674
+ * `readConfigFileSnapshot` stays private on purpose. Its `raw` carries provider
675
+ * API keys and admission tokens, and `privacy:scan` reads tracked source text,
676
+ * not runtime values — so it would not catch a caller that logged or serialized
677
+ * that string. The digest travels; the bytes do not.
678
+ */
679
+ export type ConfigAdmissionSnapshot =
680
+ | Readonly<{ kind: "read"; diagnostics: ConfigDiagnostics; contentSha256: string }>
681
+ | Readonly<{ kind: "unreadable"; diagnostics: ConfigDiagnostics; contentSha256: null }>;
682
+
683
+ export function readConfigAdmissionSnapshot(): ConfigAdmissionSnapshot {
684
+ let bytes: Buffer;
685
+ try {
686
+ // ONE read. Hashing the file and then reading it again to parse would leave
687
+ // a window for the two to disagree, which is the exact hazard this exists
688
+ // to detect — the check would become a second chance to be wrong.
689
+ bytes = readFileSync(getConfigPath());
690
+ } catch (error) {
691
+ return {
692
+ kind: "unreadable",
693
+ diagnostics: isMissingPathError(error)
694
+ ? { config: getDefaultConfig(), source: "default", error: null }
695
+ : { config: getDefaultConfig(), source: "fallback", error: "invalid_json" },
696
+ contentSha256: null,
697
+ };
698
+ }
699
+ return {
700
+ kind: "read",
701
+ // Decoded from the same buffer that was hashed, not re-read from disk.
702
+ diagnostics: configDiagnosticsFromRaw(bytes.toString("utf-8")),
703
+ contentSha256: createHash("sha256").update(bytes).digest("hex"),
704
+ };
705
+ }