@bitkyc08/opencodex 2.10.0 → 2.10.1

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 (274) hide show
  1. package/AGENTS_INSTALL.md +77 -0
  2. package/README.md +4 -10
  3. package/bin/ocx.mjs +71 -18
  4. package/gui/dist/assets/index-Cd6_PBKn.css +1 -0
  5. package/gui/dist/assets/index-ChZQsmBY.js +70 -0
  6. package/gui/dist/index.html +2 -2
  7. package/gui/dist/provider-icons/alibaba-color.svg +1 -1
  8. package/gui/dist/provider-icons/antigravity-color.svg +1 -1
  9. package/gui/dist/provider-icons/claude-color.svg +1 -1
  10. package/gui/dist/provider-icons/cline-color.svg +16 -0
  11. package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -1
  12. package/gui/dist/provider-icons/copilot-color.svg +1 -1
  13. package/gui/dist/provider-icons/cursor-color.svg +1 -1
  14. package/gui/dist/provider-icons/deepseek-color.svg +1 -1
  15. package/gui/dist/provider-icons/firepass-color.svg +1 -1
  16. package/gui/dist/provider-icons/fireworks-color.svg +1 -1
  17. package/gui/dist/provider-icons/gemini-color.svg +1 -1
  18. package/gui/dist/provider-icons/github-copilot-color.svg +1 -1
  19. package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -1
  20. package/gui/dist/provider-icons/grok.svg +1 -1
  21. package/gui/dist/provider-icons/groq-color.svg +1 -1
  22. package/gui/dist/provider-icons/huggingface-color.svg +1 -1
  23. package/gui/dist/provider-icons/kimi-color.svg +1 -1
  24. package/gui/dist/provider-icons/kiro-color.svg +2 -2
  25. package/gui/dist/provider-icons/lm-studio-color.svg +1 -1
  26. package/gui/dist/provider-icons/mistral-color.svg +1 -1
  27. package/gui/dist/provider-icons/moonshot-color.svg +1 -1
  28. package/gui/dist/provider-icons/nvidia-color.svg +1 -1
  29. package/gui/dist/provider-icons/ollama-color.svg +1 -1
  30. package/gui/dist/provider-icons/openai.svg +1 -1
  31. package/gui/dist/provider-icons/opencode.svg +2 -1
  32. package/gui/dist/provider-icons/openrouter-color.svg +1 -1
  33. package/gui/dist/provider-icons/pi.svg +2 -2
  34. package/gui/dist/provider-icons/qianfan-color.svg +1 -1
  35. package/gui/dist/provider-icons/qwen-portal-color.svg +1 -1
  36. package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -1
  37. package/gui/dist/provider-icons/vllm-color.svg +1 -1
  38. package/gui/dist/provider-icons/xiaomi-color.svg +1 -1
  39. package/package.json +8 -4
  40. package/src/adapters/anthropic.ts +208 -14
  41. package/src/adapters/base.ts +16 -5
  42. package/src/adapters/cursor/effort-map.ts +3 -2
  43. package/src/adapters/cursor/framing.ts +39 -0
  44. package/src/adapters/cursor/live-transport.ts +105 -95
  45. package/src/adapters/cursor/native-exec.ts +32 -6
  46. package/src/adapters/cursor/protobuf-request.ts +20 -15
  47. package/src/adapters/cursor/request-builder.ts +21 -7
  48. package/src/adapters/cursor/types.ts +7 -0
  49. package/src/adapters/google-antigravity-replay.ts +237 -21
  50. package/src/adapters/google-truncation.ts +11 -0
  51. package/src/adapters/google.ts +50 -9
  52. package/src/adapters/identity.ts +39 -6
  53. package/src/adapters/kiro-errors.ts +11 -0
  54. package/src/adapters/kiro-events.ts +19 -1
  55. package/src/adapters/kiro-thinking.ts +10 -2
  56. package/src/adapters/kiro-tools.ts +10 -1
  57. package/src/adapters/kiro.ts +37 -11
  58. package/src/adapters/openai-chat.ts +284 -83
  59. package/src/adapters/openai-responses.ts +182 -24
  60. package/src/bridge.ts +177 -7
  61. package/src/chat/outbound.ts +78 -23
  62. package/src/claude/agents-inject.ts +27 -5
  63. package/src/claude/inbound.ts +11 -1
  64. package/src/claude/model-info.ts +13 -10
  65. package/src/claude/outbound.ts +17 -0
  66. package/src/cli/account-api.ts +24 -0
  67. package/src/cli/account-auth.ts +31 -6
  68. package/src/cli/account-main.ts +317 -0
  69. package/src/cli/account.ts +5 -0
  70. package/src/cli/claude.ts +2 -1
  71. package/src/cli/doctor.ts +93 -22
  72. package/src/cli/export-command.ts +26 -12
  73. package/src/cli/help.ts +8 -6
  74. package/src/cli/index.ts +56 -22
  75. package/src/cli/integrations.ts +84 -1
  76. package/src/cli/observe.ts +54 -1
  77. package/src/cli/opencode.ts +2 -1
  78. package/src/cli/provider-runtime.ts +18 -1
  79. package/src/cli/route-policy.ts +92 -0
  80. package/src/cli/runtime-api.ts +6 -3
  81. package/src/cli/star-prompt.ts +71 -15
  82. package/src/cli/status.ts +1 -1
  83. package/src/cli/v2.ts +36 -9
  84. package/src/clients/config-export.ts +687 -10
  85. package/src/codex/account-lifecycle.ts +30 -5
  86. package/src/codex/account-usability.ts +22 -2
  87. package/src/codex/admission.ts +255 -0
  88. package/src/codex/auth-api.ts +427 -140
  89. package/src/codex/auth-context.ts +155 -30
  90. package/src/codex/autostart-health.ts +8 -1
  91. package/src/codex/catalog/account-models.ts +62 -0
  92. package/src/codex/catalog/aggregation.ts +14 -1
  93. package/src/codex/catalog/bundled.ts +282 -32
  94. package/src/codex/catalog/filesystem-evidence.ts +302 -0
  95. package/src/codex/catalog/metadata.ts +51 -6
  96. package/src/codex/catalog/parsing.ts +6 -3
  97. package/src/codex/catalog/provider-fetch.ts +576 -41
  98. package/src/codex/catalog/sync.ts +505 -66
  99. package/src/codex/catalog-admission.ts +197 -0
  100. package/src/codex/catalog-write-serialization.ts +241 -0
  101. package/src/codex/catalog.ts +2 -1
  102. package/src/codex/codex-write-lock.ts +372 -0
  103. package/src/codex/convergence-types.ts +593 -0
  104. package/src/codex/convergence.ts +441 -0
  105. package/src/codex/desired-state.ts +177 -0
  106. package/src/codex/features.ts +52 -8
  107. package/src/codex/generation.ts +202 -0
  108. package/src/codex/history-job.ts +257 -0
  109. package/src/codex/history-lock.ts +241 -0
  110. package/src/codex/history-migration-guardian.ts +18 -5
  111. package/src/codex/history-provider.ts +9 -2
  112. package/src/codex/history-transition.ts +105 -0
  113. package/src/codex/history-worker.ts +176 -0
  114. package/src/codex/inject-coordination.ts +245 -0
  115. package/src/codex/inject.ts +605 -124
  116. package/src/codex/integration-record.ts +266 -0
  117. package/src/codex/internal/catalog-writer.ts +203 -0
  118. package/src/codex/internal/history-writer.ts +80 -0
  119. package/src/codex/journal.ts +10 -1
  120. package/src/codex/main-account-cache.ts +24 -0
  121. package/src/codex/management-convergence.ts +114 -0
  122. package/src/codex/native-main-admission.ts +47 -0
  123. package/src/codex/native-main-auth-temp.ts +187 -0
  124. package/src/codex/native-main-claim.ts +167 -0
  125. package/src/codex/native-main-lock-file.ts +158 -0
  126. package/src/codex/native-main-owner.ts +315 -0
  127. package/src/codex/native-profile-api.ts +247 -0
  128. package/src/codex/native-profile-manager.ts +1512 -0
  129. package/src/codex/native-profile-processes.ts +121 -0
  130. package/src/codex/native-profile-recovery.ts +99 -0
  131. package/src/codex/native-profile-stage-store.ts +387 -0
  132. package/src/codex/native-profile-startup.ts +340 -0
  133. package/src/codex/native-profile-store.ts +855 -0
  134. package/src/codex/native-profile-types.ts +120 -0
  135. package/src/codex/native-residue.ts +557 -0
  136. package/src/codex/project-config-warnings.ts +18 -4
  137. package/src/codex/prompt-journal.ts +311 -0
  138. package/src/codex/prompt-layers.ts +967 -0
  139. package/src/codex/prompt-lock.ts +143 -0
  140. package/src/codex/quota-rejection.ts +224 -0
  141. package/src/codex/quota.ts +86 -3
  142. package/src/codex/routing.ts +299 -62
  143. package/src/codex/runtime.ts +159 -38
  144. package/src/codex/shim.ts +39 -13
  145. package/src/codex/subagent-model-fallback.ts +73 -12
  146. package/src/codex/transition-state.ts +604 -0
  147. package/src/codex/upstream-host-health.ts +70 -0
  148. package/src/codex/user-identity.ts +266 -0
  149. package/src/codex/write-coordination.ts +114 -0
  150. package/src/config.ts +562 -26
  151. package/src/generated/jawcode-model-metadata.ts +2 -2
  152. package/src/grok/inject.ts +15 -4
  153. package/src/grok/inspect.ts +45 -0
  154. package/src/images/loop.ts +113 -20
  155. package/src/integrations/config-io.ts +151 -0
  156. package/src/integrations/journal.ts +315 -0
  157. package/src/integrations/merge.ts +135 -0
  158. package/src/integrations/native/ownership-preflight.ts +165 -0
  159. package/src/integrations/ownership.ts +111 -0
  160. package/src/integrations/registry.ts +101 -0
  161. package/src/integrations/serialize.ts +235 -0
  162. package/src/integrations/state.ts +290 -0
  163. package/src/integrations/store.ts +103 -0
  164. package/src/integrations/writer.ts +492 -0
  165. package/src/lib/bounded-body.ts +46 -8
  166. package/src/lib/bun-runtime.ts +110 -1
  167. package/src/lib/bun-stream-caps.ts +2 -1
  168. package/src/lib/redact.ts +407 -2
  169. package/src/lib/shadow-call.ts +24 -0
  170. package/src/lib/translator-budget.ts +10 -0
  171. package/src/lib/upstream-reachability.ts +91 -0
  172. package/src/lib/upstream-retry.ts +154 -2
  173. package/src/lib/windows-secret-acl.ts +212 -11
  174. package/src/lib/winsw.ts +9 -3
  175. package/src/oauth/index.ts +61 -3
  176. package/src/oauth/key-providers.ts +4 -0
  177. package/src/oauth/kiro.ts +50 -6
  178. package/src/oauth/store.ts +31 -6
  179. package/src/oauth/token-guardian.ts +9 -3
  180. package/src/providers/codex-capacity.ts +288 -0
  181. package/src/providers/derive.ts +33 -1
  182. package/src/providers/free-directory.ts +3 -1
  183. package/src/providers/key-failover.ts +71 -3
  184. package/src/providers/openai-sidecar.ts +64 -4
  185. package/src/providers/openai-virtual-models.ts +1 -0
  186. package/src/providers/quota.ts +334 -26
  187. package/src/providers/registry.ts +284 -16
  188. package/src/providers/xai-transport.ts +11 -4
  189. package/src/responses/compaction.ts +8 -1
  190. package/src/responses/hosted-tool-policy.ts +9 -0
  191. package/src/responses/parser.ts +44 -2
  192. package/src/responses/reasoning-envelope.ts +9 -1
  193. package/src/responses/reasoning-replay-cache.ts +105 -0
  194. package/src/responses/spill-store.ts +45 -8
  195. package/src/responses/state.ts +161 -17
  196. package/src/router.ts +302 -16
  197. package/src/routing/analytics.ts +377 -0
  198. package/src/routing/capability.ts +204 -0
  199. package/src/routing/cost.ts +77 -0
  200. package/src/routing/evaluator.ts +444 -0
  201. package/src/routing/health.ts +401 -0
  202. package/src/routing/history/cursor.ts +43 -0
  203. package/src/routing/history/indexer.ts +590 -0
  204. package/src/routing/history/schema.ts +72 -0
  205. package/src/routing/profile.ts +423 -0
  206. package/src/routing/quota.ts +124 -0
  207. package/src/routing/request-evidence.ts +45 -0
  208. package/src/routing/trace.ts +686 -0
  209. package/src/server/auth-cors.ts +46 -6
  210. package/src/server/chat-completions.ts +28 -13
  211. package/src/server/claude-messages.ts +23 -15
  212. package/src/server/gui-static.ts +39 -10
  213. package/src/server/images.ts +10 -1
  214. package/src/server/index.ts +238 -52
  215. package/src/server/lifecycle.ts +228 -9
  216. package/src/server/live.ts +14 -3
  217. package/src/server/management/agent-settings-routes.ts +64 -14
  218. package/src/server/management/combo-routes.ts +5 -5
  219. package/src/server/management/config-routes.ts +1 -1
  220. package/src/server/management/context.ts +42 -2
  221. package/src/server/management/integration-routes.ts +538 -0
  222. package/src/server/management/logs-usage-routes.ts +1 -1
  223. package/src/server/management/model-routes.ts +32 -113
  224. package/src/server/management/model-rows.ts +117 -0
  225. package/src/server/management/native-integration-routes.ts +587 -0
  226. package/src/server/management/oauth-account-routes.ts +1 -1
  227. package/src/server/management/provider-routes.ts +218 -117
  228. package/src/server/management/request-history-routes.ts +191 -0
  229. package/src/server/management/routing-analytics-routes.ts +74 -0
  230. package/src/server/management/routing-profile-routes.ts +177 -0
  231. package/src/server/management/shared.ts +2 -2
  232. package/src/server/management/sidebar-routes.ts +47 -31
  233. package/src/server/management/sync-response.ts +69 -0
  234. package/src/server/management/system-restart.ts +276 -24
  235. package/src/server/management/system-routes.ts +4 -0
  236. package/src/server/management-api.ts +84 -9
  237. package/src/server/management-auth.ts +43 -5
  238. package/src/server/relay-eager.ts +82 -42
  239. package/src/server/relay.ts +120 -6
  240. package/src/server/request-log.ts +26 -6
  241. package/src/server/responses/collaboration.ts +63 -8
  242. package/src/server/responses/compact.ts +272 -41
  243. package/src/server/responses/core.ts +730 -132
  244. package/src/server/responses/fetch-helpers.ts +15 -1
  245. package/src/server/responses-item-id-repair.ts +32 -3
  246. package/src/server/responses-json-events.ts +52 -0
  247. package/src/server/responses-snapshot-repair.ts +621 -0
  248. package/src/server/search.ts +51 -6
  249. package/src/server/sse-payload-rewrite.ts +89 -12
  250. package/src/server/startup-health-cache.ts +7 -1
  251. package/src/server/ws-bridge.ts +11 -17
  252. package/src/service-manager-probe.ts +297 -0
  253. package/src/service.ts +222 -32
  254. package/src/tray/windows-tray.ps1 +9 -0
  255. package/src/tray/windows.ts +15 -7
  256. package/src/types.ts +194 -14
  257. package/src/update/index.ts +13 -13
  258. package/src/update/job.ts +24 -21
  259. package/src/update/notify.ts +7 -3
  260. package/src/usage/cost.ts +0 -0
  261. package/src/usage/expected-prices.ts +129 -10
  262. package/src/usage/log.ts +50 -15
  263. package/src/usage/summary.ts +4 -4
  264. package/src/vision/index.ts +6 -1
  265. package/src/web-search/loop.ts +161 -34
  266. package/gui/dist/assets/index-OY43ubAq.css +0 -1
  267. package/gui/dist/assets/index-YwNnKZcL.js +0 -67
  268. package/gui/dist/provider-icons/antigravity.svg +0 -1
  269. package/gui/dist/provider-icons/claude.svg +0 -1
  270. package/gui/dist/provider-icons/copilot.svg +0 -1
  271. package/gui/dist/provider-icons/cursor.svg +0 -2
  272. package/gui/dist/provider-icons/gemini.svg +0 -1
  273. package/gui/dist/provider-icons/grok-color.svg +0 -1
  274. package/gui/dist/provider-icons/kiro.svg +0 -14
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Minimal catalog admission for the management refresh path.
3
+ *
4
+ * The r2 #1 catalog incident left gather and native writes in one awaited
5
+ * callback. WP9 needs generation and filesystem-target evidence before it can
6
+ * split those phases, but importing WP12 authority would make WP9 depend on a
7
+ * later phase. This reader therefore captures only the exact resident config,
8
+ * its cooperating generation, and identities for catalog-owned targets.
9
+ */
10
+ import { createHmac, randomBytes } from "node:crypto";
11
+ import { join, resolve } from "node:path";
12
+
13
+ import { observeConfigGeneration } from "../config";
14
+ import type { OcxConfig } from "../types";
15
+ import type {
16
+ CatalogAdmissionSnapshot,
17
+ CatalogConvergeRequestInput,
18
+ ConfigGeneration,
19
+ ConvergeRequest,
20
+ } from "./convergence-types";
21
+ import {
22
+ catalogBackupPathFor,
23
+ legacyCatalogBackupPath,
24
+ samePath,
25
+ } from "./catalog/parsing";
26
+ import { readRootTomlString } from "./paths";
27
+ import {
28
+ acceptCatalogGatherSourcePath,
29
+ captureAndSealCatalogHomeSelection,
30
+ captureCatalogGatherTargetIdentity,
31
+ createCatalogGatherEvidenceSession,
32
+ readCatalogGatherSource,
33
+ sealCatalogGatherEvidenceSession,
34
+ } from "./catalog/filesystem-evidence";
35
+
36
+ /**
37
+ * Construct the one request shape permitted for management catalog refreshes.
38
+ * Callers choose the deadline only; they cannot widen scope or choose direction.
39
+ */
40
+ export function createCatalogConvergeRequest({
41
+ deadlineMs,
42
+ }: CatalogConvergeRequestInput): ConvergeRequest {
43
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0) {
44
+ throw new TypeError("Catalog convergence deadlineMs must be a positive safe integer.");
45
+ }
46
+
47
+ return {
48
+ action: "converge",
49
+ scope: "catalog",
50
+ reason: "management-mutation",
51
+ mode: "automatic",
52
+ deadlineMs,
53
+ };
54
+ }
55
+
56
+ const CONFIG_IDENTITY_KEY = randomBytes(32);
57
+ const configReferenceIdentities = new WeakMap<object, string>();
58
+ let nextConfigReferenceIdentity = 0;
59
+
60
+ function encodeLengthPrefixed(value: string): string {
61
+ return `${Buffer.byteLength(value, "utf8")}:${value}`;
62
+ }
63
+
64
+ function canonicalConfigEncoding(value: unknown, ancestors = new Set<object>()): string {
65
+ if (value === null) return "null";
66
+ switch (typeof value) {
67
+ case "undefined": return "undefined";
68
+ case "boolean": return value ? "boolean:1" : "boolean:0";
69
+ case "string": return `string:${encodeLengthPrefixed(value)}`;
70
+ case "number": {
71
+ if (!Number.isFinite(value)) throw new TypeError("Catalog config identity cannot encode a non-finite number.");
72
+ return `number:${Object.is(value, -0) ? "-0" : String(value)}`;
73
+ }
74
+ case "bigint":
75
+ case "function":
76
+ case "symbol":
77
+ throw new TypeError(`Catalog config identity cannot encode ${typeof value}.`);
78
+ case "object": break;
79
+ }
80
+
81
+ if (ancestors.has(value)) throw new TypeError("Catalog config identity cannot encode a cyclic graph.");
82
+ if (Object.getOwnPropertySymbols(value).length > 0) {
83
+ throw new TypeError("Catalog config identity cannot encode symbol keys.");
84
+ }
85
+ ancestors.add(value);
86
+ try {
87
+ if (Array.isArray(value)) {
88
+ const items = Array.from({ length: value.length }, (_, index) => (
89
+ Object.hasOwn(value, index)
90
+ ? `item:${canonicalConfigEncoding(value[index], ancestors)}`
91
+ : "hole"
92
+ ));
93
+ return `array:${value.length}:${items.map(encodeLengthPrefixed).join("")}`;
94
+ }
95
+ const keys = Object.keys(value).sort();
96
+ const entries = keys.map(key => {
97
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
98
+ if (!descriptor || !("value" in descriptor)) {
99
+ throw new TypeError("Catalog config identity cannot encode accessor properties.");
100
+ }
101
+ return `${encodeLengthPrefixed(key)}${encodeLengthPrefixed(canonicalConfigEncoding(descriptor.value, ancestors))}`;
102
+ });
103
+ return `object:${keys.length}:${entries.join("")}`;
104
+ } finally {
105
+ ancestors.delete(value);
106
+ }
107
+ }
108
+
109
+ function keyedConfigIdentity(domain: string, payload: string): string {
110
+ return createHmac("sha256", CONFIG_IDENTITY_KEY)
111
+ .update(encodeLengthPrefixed(domain))
112
+ .update(encodeLengthPrefixed(payload))
113
+ .digest("hex");
114
+ }
115
+
116
+ function catalogConfigIdentity(
117
+ config: Readonly<OcxConfig>,
118
+ generation: ConfigGeneration,
119
+ ): CatalogAdmissionSnapshot["configIdentity"] {
120
+ let referenceIdentity = configReferenceIdentities.get(config);
121
+ if (!referenceIdentity) {
122
+ nextConfigReferenceIdentity += 1;
123
+ referenceIdentity = keyedConfigIdentity("catalog-config-reference-v1", String(nextConfigReferenceIdentity));
124
+ configReferenceIdentities.set(config, referenceIdentity);
125
+ }
126
+ return Object.freeze({
127
+ referenceIdentity,
128
+ generation: Object.freeze({ ...generation }),
129
+ snapshotIdentity: keyedConfigIdentity("catalog-config-snapshot-v1", canonicalConfigEncoding(config)),
130
+ });
131
+ }
132
+
133
+ /**
134
+ * Capture the catalog-only evidence WP9 can validate without consulting WP12.
135
+ * The config reference is retained verbatim; no persisted config re-read may
136
+ * replace the object already held by the management callback.
137
+ */
138
+ export function captureCatalogAdmissionSnapshot(
139
+ config: Readonly<OcxConfig>,
140
+ ): CatalogAdmissionSnapshot {
141
+ const generation = observeConfigGeneration();
142
+ if (generation.kind === "absent") {
143
+ /*
144
+ * Absence is refused HERE and admitted elsewhere, and the difference is the
145
+ * lock. Codex write admission may carry an absent generation because it goes
146
+ * on to take the config transaction and read a real zero inside it. Catalog
147
+ * gather has no such transaction — by contract it holds no lock and writes
148
+ * nothing — so it has no way to turn absence into an observation. Refusing
149
+ * is the only honest answer available to it.
150
+ *
151
+ * The wording matters beyond this line: `admissionFailure` in
152
+ * management-convergence.ts classifies this throw by matching its MESSAGE,
153
+ * and an unrecognized one becomes a non-retryable `failed/disk` instead of
154
+ * the retryable skip a missing coordinator has always produced. Keeping the
155
+ * "config generation is database" phrasing preserves that projection — a
156
+ * caller that could simply try again should still be told to.
157
+ */
158
+ throw new Error("Cannot capture Codex catalog admission: config generation is database (absent).");
159
+ }
160
+ if (generation.kind !== "ready") {
161
+ throw new Error(`Cannot capture Codex catalog admission: config generation is ${generation.reason}.`);
162
+ }
163
+
164
+ const evidenceSession = createCatalogGatherEvidenceSession();
165
+ const homeSelection = captureAndSealCatalogHomeSelection(evidenceSession);
166
+ const configPath = join(homeSelection.canonicalCodexHome, "config.toml");
167
+ acceptCatalogGatherSourcePath(evidenceSession, "catalog-target-selection", configPath);
168
+ const configBytes = readCatalogGatherSource(evidenceSession, "catalog-target-selection");
169
+ const configuredCatalogPath = configBytes === null
170
+ ? null
171
+ : readRootTomlString(Buffer.from(configBytes).toString("utf8"), "model_catalog_json");
172
+ const defaultCatalogPath = join(homeSelection.canonicalCodexHome, "opencodex-catalog.json");
173
+ const catalogPath = configuredCatalogPath
174
+ ? resolve(homeSelection.canonicalCodexHome, configuredCatalogPath)
175
+ : defaultCatalogPath;
176
+ const backupPaths = [
177
+ catalogBackupPathFor(catalogPath),
178
+ ...(samePath(catalogPath, defaultCatalogPath) ? [legacyCatalogBackupPath()] : []),
179
+ ];
180
+ const targets = {
181
+ catalog: captureCatalogGatherTargetIdentity(evidenceSession, catalogPath),
182
+ cache: captureCatalogGatherTargetIdentity(
183
+ evidenceSession,
184
+ join(homeSelection.canonicalCodexHome, "models_cache.json"),
185
+ ),
186
+ catalogBackups: backupPaths.map(path => captureCatalogGatherTargetIdentity(evidenceSession, path)),
187
+ };
188
+ const sourceEvidence = sealCatalogGatherEvidenceSession(evidenceSession);
189
+
190
+ return {
191
+ config,
192
+ generation: generation.generation,
193
+ configIdentity: catalogConfigIdentity(config, generation.generation),
194
+ targets,
195
+ sourceEvidence,
196
+ };
197
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * K — the permanent catalog serialization primitive.
3
+ *
4
+ * Round 4 of the audit killed the idea that "the replacement happens under a
5
+ * lock" is enough. The retained `/api/sync` chain reads the active catalog,
6
+ * captures it, awaits provider gathering, and only then writes from that
7
+ * captured state (`src/codex/catalog/sync.ts:513,520,526,565`). If convergence
8
+ * publishes Y while that await is pending, taking a lock afterwards does not
9
+ * make the captured X fresh — the writer legally overwrites Y while holding it.
10
+ * So K serializes the whole read-transform-write transaction, either by
11
+ * recomputing under K or by revalidating complete evidence under K, not by
12
+ * guarding the last rename.
13
+ *
14
+ * The second defect that round found was subtler: an opaque TypeScript permit
15
+ * type proves a permit-bearing call path EXISTS. It cannot prove the callback
16
+ * still holds K. A leaked permit used after its callback returned type-checks
17
+ * perfectly. So the type is not the mechanism — this module owns a private
18
+ * active-permit registry, and every mutator asks it at runtime whether the
19
+ * permit it holds is still live for the home it is about to write.
20
+ *
21
+ * K is permanent and is NOT WP11's native write lock. It never reads or
22
+ * advances the native pair. Order is `N -> K -> C`; there is no `C -> K` and no
23
+ * `K -> N` (`005_contract.md:660-762`).
24
+ *
25
+ * Design record: devlog/_plan/260804_codex_write_substrate/005_contract.md §3.
26
+ */
27
+ import { chmodSync, lstatSync, realpathSync } from "node:fs";
28
+
29
+ import { Database } from "bun:sqlite";
30
+
31
+ import {
32
+ CodexUserIdentityRefusal,
33
+ resolveCodexCatalogSerializationDatabasePath,
34
+ resolveEffectiveUserIdentity,
35
+ } from "./user-identity";
36
+
37
+ /**
38
+ * Authorization to perform the fixed sequence of catalog mutations inside ONE
39
+ * K acquisition.
40
+ *
41
+ * Deliberately carries no usable field. Holding this object is necessary but
42
+ * never sufficient: `assertCatalogWritePermit` is what actually decides, by
43
+ * looking the object up in a registry this module alone can write. A forged
44
+ * cast, a prototype copy, or a symbol clone produces a value of this type that
45
+ * every writer refuses.
46
+ */
47
+ export interface CatalogWritePermit {
48
+ readonly [catalogWritePermitBrand]: true;
49
+ }
50
+
51
+ declare const catalogWritePermitBrand: unique symbol;
52
+
53
+ export type CatalogSerializationOutcome<T> =
54
+ | { kind: "completed"; value: T }
55
+ | { kind: "unavailable"; reason: "busy" | "database" | "unsafe-path" };
56
+
57
+ export class CatalogWritePermitRefusal extends Error {
58
+ readonly code = "CODEX_CATALOG_WRITE_PERMIT_REFUSED";
59
+
60
+ constructor(message: string) {
61
+ super(message);
62
+ this.name = "CatalogWritePermitRefusal";
63
+ }
64
+ }
65
+
66
+ interface PermitRegistration {
67
+ /** The exact canonical CODEX_HOME whose artifacts this acquisition may write. */
68
+ readonly canonicalCodexHome: string;
69
+ /** Identifies the acquisition, so a permit cannot outlive its transaction. */
70
+ readonly transactionId: string;
71
+ live: boolean;
72
+ }
73
+
74
+ /**
75
+ * The registry is a module-private WeakMap keyed by permit IDENTITY. There is
76
+ * no exported constructor, brand value, or registration API, so the only way to
77
+ * obtain a registered permit is to be inside a live callback.
78
+ */
79
+ const activePermits = new WeakMap<object, PermitRegistration>();
80
+
81
+ let acquisitionCounter = 0;
82
+
83
+ function isBusy(error: unknown): boolean {
84
+ const code = error && typeof error === "object" && "code" in error
85
+ ? String((error as { code?: unknown }).code)
86
+ : "";
87
+ const message = error instanceof Error ? error.message : String(error);
88
+ return code === "SQLITE_BUSY" || code === "SQLITE_LOCKED"
89
+ || /database (?:is|table is) locked/i.test(message);
90
+ }
91
+
92
+ /**
93
+ * The runtime proof every low-level catalog/backup/cache mutator must obtain
94
+ * before its FIRST filesystem mutation — including temp creation, hardening,
95
+ * unlink, link, rename, truncate, or replacement.
96
+ *
97
+ * `owningCodexHome` is supplied by the caller rather than inferred from the
98
+ * target's parent directory, because an accepted configured catalog target may
99
+ * be absolute and outside CODEX_HOME entirely.
100
+ */
101
+ export function assertCatalogWritePermit(
102
+ permit: CatalogWritePermit,
103
+ owningCodexHome: string,
104
+ ): void {
105
+ const registration = activePermits.get(permit as unknown as object);
106
+ if (!registration) {
107
+ throw new CatalogWritePermitRefusal(
108
+ "The catalog write permit was not minted by the serialization owner.",
109
+ );
110
+ }
111
+ if (!registration.live) {
112
+ throw new CatalogWritePermitRefusal(
113
+ "The catalog write permit belongs to a released acquisition.",
114
+ );
115
+ }
116
+ if (registration.canonicalCodexHome !== owningCodexHome) {
117
+ throw new CatalogWritePermitRefusal(
118
+ "The catalog write permit authorizes a different CODEX_HOME.",
119
+ );
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Acquire K for one canonical CODEX_HOME and run `write` while it is held.
125
+ *
126
+ * Synchronous by contract: the callback performs no provider request, runtime
127
+ * probe, OAuth refresh, subprocess, or awaited work. It may enter
128
+ * `withExpectedConfigGenerationSync` — that is the `K -> C` edge.
129
+ *
130
+ * `busy_timeout = 0` with `BEGIN IMMEDIATE` makes contention fail fast and
131
+ * typed; the outer async orchestration decides whether to retry within its
132
+ * deadline. Blocking here would hold K across an unbounded wait.
133
+ */
134
+ export function withCatalogWriteSerialization<T>(
135
+ canonicalCodexHome: string,
136
+ write: (permit: CatalogWritePermit) => T,
137
+ ): CatalogSerializationOutcome<T> {
138
+ let databasePath: string;
139
+ try {
140
+ databasePath = resolveCodexCatalogSerializationDatabasePath(
141
+ resolveEffectiveUserIdentity(),
142
+ canonicalCodexHome,
143
+ );
144
+ } catch (error) {
145
+ if (error instanceof CodexUserIdentityRefusal) {
146
+ return { kind: "unavailable", reason: "unsafe-path" };
147
+ }
148
+ return { kind: "unavailable", reason: "database" };
149
+ }
150
+
151
+ let database: Database | undefined;
152
+ let transactionOpen = false;
153
+ let registration: PermitRegistration | undefined;
154
+ let permit: CatalogWritePermit | undefined;
155
+
156
+ try {
157
+ let databaseWasAbsent = false;
158
+ try {
159
+ const before = lstatSync(databasePath);
160
+ if (before.isSymbolicLink() || !before.isFile()) {
161
+ return { kind: "unavailable", reason: "unsafe-path" };
162
+ }
163
+ if (process.platform !== "win32") {
164
+ const uid = process.getuid?.();
165
+ if (uid === undefined || before.uid !== uid || (before.mode & 0o777) !== 0o600) {
166
+ return { kind: "unavailable", reason: "unsafe-path" };
167
+ }
168
+ }
169
+ } catch (cause) {
170
+ const code = cause && typeof cause === "object" && "code" in cause
171
+ ? String((cause as { code?: unknown }).code)
172
+ : "";
173
+ if (code !== "ENOENT") throw cause;
174
+ databaseWasAbsent = true;
175
+ }
176
+
177
+ database = new Database(databasePath, { create: true });
178
+ if (databaseWasAbsent) {
179
+ try { chmodSync(databasePath, 0o600); } catch { /* Windows applies ACLs in WP11. */ }
180
+ }
181
+ const opened = lstatSync(databasePath);
182
+ if (opened.isSymbolicLink() || !opened.isFile()
183
+ || realpathSync.native(databasePath) !== databasePath) {
184
+ return { kind: "unavailable", reason: "unsafe-path" };
185
+ }
186
+
187
+ database.exec("PRAGMA busy_timeout = 0; BEGIN IMMEDIATE");
188
+ transactionOpen = true;
189
+
190
+ acquisitionCounter += 1;
191
+ registration = {
192
+ canonicalCodexHome,
193
+ transactionId: `${process.pid}:${acquisitionCounter}`,
194
+ live: true,
195
+ };
196
+ // A bare object: nothing about it is guessable or reconstructable, because
197
+ // authority lives in the registry entry rather than in the value.
198
+ permit = {} as CatalogWritePermit;
199
+ activePermits.set(permit as unknown as object, registration);
200
+
201
+ let value: T;
202
+ try {
203
+ value = write(permit);
204
+ } finally {
205
+ // Revoke BEFORE the transaction is released, so a mutator racing the
206
+ // release can never find a live permit without a live lock. This runs on
207
+ // the throwing path too, which is the case a `finally`-less version gets
208
+ // wrong.
209
+ registration.live = false;
210
+ activePermits.delete(permit as unknown as object);
211
+ }
212
+
213
+ database.exec("COMMIT");
214
+ transactionOpen = false;
215
+ return { kind: "completed", value };
216
+ } catch (error) {
217
+ if (transactionOpen) {
218
+ try { database?.exec("ROLLBACK"); } catch { /* close releases the transaction */ }
219
+ transactionOpen = false;
220
+ }
221
+ if (registration?.live) {
222
+ registration.live = false;
223
+ if (permit) activePermits.delete(permit as unknown as object);
224
+ }
225
+ if (error instanceof CodexUserIdentityRefusal) {
226
+ return { kind: "unavailable", reason: "unsafe-path" };
227
+ }
228
+ if (isBusy(error)) return { kind: "unavailable", reason: "busy" };
229
+ // A callback failure is the caller's error, not a lock outcome: K acquired
230
+ // fine. Reporting it as `unavailable` would tell the caller to retry
231
+ // something that will fail identically.
232
+ throw error;
233
+ } finally {
234
+ try { database?.close(); } catch { /* acquisition already finished */ }
235
+ }
236
+ }
237
+
238
+ /** Test-only: prove a leaked permit is dead without reaching into the registry. */
239
+ export function isCatalogWritePermitLive(permit: CatalogWritePermit): boolean {
240
+ return activePermits.get(permit as unknown as object)?.live === true;
241
+ }
@@ -2,7 +2,7 @@
2
2
  // Public surface preserved exactly; importers keep using "src/codex/catalog".
3
3
  export { isMediaGenerationModelId, shouldExposeRoutedModel, readCodexCatalogPath, readCatalog, normalizeRoutedCatalogEntry, catalogModelSlug, filterSupportedNativeSlugs, catalogModelSupportsReasoningSummaries } from "./catalog/parsing";
4
4
  export type { CatalogModel, MultiAgentMode } from "./catalog/parsing";
5
- export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort } from "./catalog/metadata";
5
+ export { NATIVE_OPENAI_MODELS, nativeOpenAiContextWindow, disabledNativeSlugs, visibleNativeSlugs, desktopVisibleNativeSlugs, nativeModelRows, applyNativeVisibility, upstreamNativeEntry, nativeOpenAiSlugs, listCatalogNativeSlugs, nativeReasoningEfforts, nativeDefaultReasoningEffort, shouldIncludeAccountBoundNativeOpenAi, shouldIncludeNativeOpenAi } from "./catalog/metadata";
6
6
  export { isSpawnableCodexCandidate, codexExecInvocation, loadBundledCodexCatalog, materializeBundledCodexCatalog, loadCatalogTemplate } from "./catalog/bundled";
7
7
  export { nativeEffortClamp, shouldApplyNativeEffortClamp, catalogModelEfforts, codexSupportedReasoningEfforts, clampedDefaultEffort, clampEntryToCodexSupportedEfforts, clampCatalogModelsToCodexSupport } from "./catalog/effort";
8
8
  export { applyProviderConfigHints, isDatedVariantId, filterCatalogVisibleModels, gatherRoutedModels, clearGatherRoutedModelsInflight, augmentRoutedModelsWithRegistryOpenAiApiRows, augmentRoutedModelsWithJawcodeMetadata } from "./catalog/provider-fetch";
@@ -10,3 +10,4 @@ export { deriveComboCatalogModel, exactComboCatalogSlugs, getLastComboCatalogOmi
10
10
  export type { ComboCatalogOmission, ComboCatalogOmissionReason } from "./catalog/aggregation";
11
11
  export { MAX_SPAWN_AGENT_MODEL_OVERRIDES, effectiveSubagentRoster, buildCatalogEntries, resetCatalogRuntimeStateForTests, orderForSubagents, mergeCatalogEntriesForSync, syncCatalogModels, restoreCodexCatalog, invalidateCodexModelsCache } from "./catalog/sync";
12
12
  export type { SpawnAgentSurface, SubagentRosterExclusionReason, EffectiveSubagentModel, SubagentRosterExclusion, EffectiveSubagentRoster } from "./catalog/sync";
13
+ export { accountBoundNativeDisplayName, accountBoundNativeModelSlugs, CODEX_ACCOUNT_BOUND_CATALOG_KIND, trustedAccountBoundNativeCatalogSlug, visibleCodexAccountSelectors } from "./catalog/account-models";