@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,266 @@
1
+ /**
2
+ * The sole reader/writer for integrations/codex.json.
3
+ *
4
+ * Provenance once disappeared when a corrupt record was treated as an empty one
5
+ * (005_disable_leaves_a_broken_file.md). Reads therefore validate the complete
6
+ * known shape, and updates refuse malformed bytes instead of manufacturing a new
7
+ * baseline over evidence we can no longer trust.
8
+ */
9
+ import { mkdirSync, readFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { atomicWriteFile, getConfigDir, withConfigMutationLockSync } from "../config";
12
+ import type {
13
+ CodexArtifactId,
14
+ CodexIntegrationRecord,
15
+ CodexProvenanceEntry,
16
+ CodexProvenanceLedger,
17
+ } from "./convergence-types";
18
+
19
+ const RECORD_FILENAME = "codex.json";
20
+ const LEGACY_TRANSITION_KEYS = new Set([
21
+ "nativeGeneration",
22
+ "currentTxId",
23
+ "generation",
24
+ "history",
25
+ "historySchedule",
26
+ ]);
27
+
28
+ function recordPath(): string {
29
+ return join(getConfigDir(), "integrations", RECORD_FILENAME);
30
+ }
31
+
32
+ function isPlainRecord(value: unknown): value is Record<string, unknown> {
33
+ return typeof value === "object" && value !== null && !Array.isArray(value);
34
+ }
35
+
36
+ function isMissingPathError(error: unknown): boolean {
37
+ return error instanceof Error && "code" in error && (error as NodeJS.ErrnoException).code === "ENOENT";
38
+ }
39
+
40
+ function invalid(message: string) {
41
+ return { kind: "invalid" as const, message };
42
+ }
43
+
44
+ function validateArtifact(value: unknown): value is CodexArtifactId {
45
+ if (!isPlainRecord(value) || typeof value.kind !== "string") return false;
46
+ switch (value.kind) {
47
+ case "config":
48
+ case "generated-profile":
49
+ case "models-cache":
50
+ case "injection-journal":
51
+ return true;
52
+ case "active-catalog":
53
+ case "history-manifest":
54
+ case "history-rollout":
55
+ return typeof value.canonicalPath === "string"
56
+ && (value.kind === "active-catalog" || typeof value.stateDbId === "string");
57
+ case "catalog-backup":
58
+ return (value.form === "hashed" || value.form === "legacy")
59
+ && typeof value.canonicalPath === "string";
60
+ case "history-row":
61
+ case "history-manifest-entry":
62
+ return typeof value.stateDbId === "string" && typeof value.threadId === "string";
63
+ default:
64
+ return false;
65
+ }
66
+ }
67
+
68
+ function validateBaseline(value: unknown): boolean {
69
+ if (!isPlainRecord(value)) return false;
70
+ if (value.kind === "absent") return true;
71
+ return value.kind === "present"
72
+ && typeof value.sha256 === "string"
73
+ && typeof value.bytesBase64 === "string";
74
+ }
75
+
76
+ function validateEntry(value: unknown): value is CodexProvenanceEntry {
77
+ return isPlainRecord(value)
78
+ && validateArtifact(value.artifact)
79
+ && validateBaseline(value.baseline)
80
+ && (typeof value.postImage === "string" || value.postImage === null)
81
+ && typeof value.txId === "string"
82
+ && typeof value.at === "string";
83
+ }
84
+
85
+ function validateLedger(value: unknown): value is CodexProvenanceLedger {
86
+ return isPlainRecord(value)
87
+ && Array.isArray(value.entries)
88
+ && value.entries.every(validateEntry);
89
+ }
90
+
91
+ function validateRecord(value: unknown): value is CodexIntegrationRecord {
92
+ return isPlainRecord(value)
93
+ && value.version === 1
94
+ && !Object.keys(value).some(key => LEGACY_TRANSITION_KEYS.has(key))
95
+ && (value.provenance === undefined || validateLedger(value.provenance));
96
+ }
97
+
98
+ function readIntegrationRecordUnlocked() {
99
+ let raw: string;
100
+ try {
101
+ raw = readFileSync(recordPath(), "utf8");
102
+ } catch (error) {
103
+ if (isMissingPathError(error)) return { kind: "missing" as const, record: null };
104
+ return invalid(`Codex integration record is unreadable: ${error instanceof Error ? error.message : String(error)}`);
105
+ }
106
+
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(raw.replace(/^\uFEFF/, ""));
110
+ } catch {
111
+ return invalid("Codex integration record contains invalid JSON");
112
+ }
113
+ if (!validateRecord(parsed)) {
114
+ return invalid("Codex integration record has an unsupported or malformed v1 shape");
115
+ }
116
+ return { kind: "ready" as const, record: parsed };
117
+ }
118
+
119
+ export const readIntegrationRecord = readIntegrationRecordUnlocked;
120
+
121
+ function copyUnknown(
122
+ previous: Record<string, unknown>,
123
+ next: Record<string, unknown>,
124
+ knownKeys: ReadonlySet<string>,
125
+ ): Record<string, unknown> {
126
+ const merged: Record<string, unknown> = {};
127
+ for (const [key, value] of Object.entries(previous)) {
128
+ if (!knownKeys.has(key)) merged[key] = value;
129
+ }
130
+ return { ...merged, ...next };
131
+ }
132
+
133
+ function artifactKnownKeys(artifact: CodexArtifactId): ReadonlySet<string> {
134
+ switch (artifact.kind) {
135
+ case "config":
136
+ case "generated-profile":
137
+ case "models-cache":
138
+ case "injection-journal":
139
+ return new Set(["kind"]);
140
+ case "active-catalog":
141
+ return new Set(["kind", "canonicalPath"]);
142
+ case "catalog-backup":
143
+ return new Set(["kind", "form", "canonicalPath"]);
144
+ case "history-row":
145
+ case "history-manifest-entry":
146
+ return new Set(["kind", "stateDbId", "threadId"]);
147
+ case "history-manifest":
148
+ case "history-rollout":
149
+ return new Set(["kind", "stateDbId", "canonicalPath"]);
150
+ }
151
+ }
152
+
153
+ function mergeArtifact(previous: CodexArtifactId, next: CodexArtifactId): CodexArtifactId {
154
+ return copyUnknown(
155
+ previous as CodexArtifactId & Record<string, unknown>,
156
+ next as CodexArtifactId & Record<string, unknown>,
157
+ artifactKnownKeys(previous),
158
+ ) as CodexArtifactId;
159
+ }
160
+
161
+ function mergeBaseline(
162
+ previous: CodexProvenanceEntry["baseline"],
163
+ next: CodexProvenanceEntry["baseline"],
164
+ ): CodexProvenanceEntry["baseline"] {
165
+ const known = previous.kind === "present"
166
+ ? new Set(["kind", "sha256", "bytesBase64"])
167
+ : new Set(["kind"]);
168
+ return copyUnknown(previous, next, known) as CodexProvenanceEntry["baseline"];
169
+ }
170
+
171
+ function knownArtifactIdentity(artifact: CodexArtifactId): string {
172
+ switch (artifact.kind) {
173
+ case "config":
174
+ case "generated-profile":
175
+ case "models-cache":
176
+ case "injection-journal":
177
+ return artifact.kind;
178
+ case "active-catalog":
179
+ return `${artifact.kind}\0${artifact.canonicalPath}`;
180
+ case "catalog-backup":
181
+ return `${artifact.kind}\0${artifact.form}\0${artifact.canonicalPath}`;
182
+ case "history-row":
183
+ case "history-manifest-entry":
184
+ return `${artifact.kind}\0${artifact.stateDbId}\0${artifact.threadId}`;
185
+ case "history-manifest":
186
+ case "history-rollout":
187
+ return `${artifact.kind}\0${artifact.stateDbId}\0${artifact.canonicalPath}`;
188
+ }
189
+ }
190
+
191
+ function mergeEntry(previous: CodexProvenanceEntry, next: CodexProvenanceEntry): CodexProvenanceEntry {
192
+ const merged = copyUnknown(
193
+ previous,
194
+ next,
195
+ new Set(["artifact", "baseline", "postImage", "txId", "at"]),
196
+ );
197
+ return {
198
+ ...merged,
199
+ artifact: mergeArtifact(previous.artifact, next.artifact),
200
+ baseline: mergeBaseline(previous.baseline, next.baseline),
201
+ } as CodexProvenanceEntry;
202
+ }
203
+
204
+ function mergeLedger(previous: CodexProvenanceLedger, next: CodexProvenanceLedger): CodexProvenanceLedger {
205
+ const unused = new Set(previous.entries.map((_, index) => index));
206
+ const entries = next.entries.map((entry, nextIndex) => {
207
+ let previousIndex = previous.entries.findIndex((candidate, index) =>
208
+ unused.has(index)
209
+ && candidate.txId === entry.txId
210
+ && candidate.at === entry.at
211
+ && knownArtifactIdentity(candidate.artifact) === knownArtifactIdentity(entry.artifact));
212
+ if (previousIndex < 0 && unused.has(nextIndex)) previousIndex = nextIndex;
213
+ if (previousIndex < 0) return entry;
214
+ unused.delete(previousIndex);
215
+ return mergeEntry(previous.entries[previousIndex]!, entry);
216
+ });
217
+ return {
218
+ ...copyUnknown(previous, next, new Set(["entries"])),
219
+ entries,
220
+ } as CodexProvenanceLedger;
221
+ }
222
+
223
+ function preserveExtensions(
224
+ previous: CodexIntegrationRecord,
225
+ next: CodexIntegrationRecord,
226
+ ): CodexIntegrationRecord {
227
+ const merged = copyUnknown(previous, next, new Set(["version", "provenance"]));
228
+ if (previous.provenance && next.provenance) {
229
+ merged.provenance = mergeLedger(previous.provenance, next.provenance);
230
+ }
231
+ return merged as CodexIntegrationRecord;
232
+ }
233
+
234
+ /**
235
+ * Keep read, extension merge, and atomic replacement inside the config mutation
236
+ * coordinator. WP12 must call this function instead of recreating the incident's
237
+ * stale read/merge/write sequence at its own call site.
238
+ */
239
+ export const updateIntegrationRecord = (
240
+ mutate: (record: CodexIntegrationRecord) => CodexIntegrationRecord,
241
+ ) => {
242
+ try {
243
+ return withConfigMutationLockSync(() => {
244
+ const read = readIntegrationRecordUnlocked();
245
+ if (read.kind === "invalid") return read;
246
+ const previous: CodexIntegrationRecord = read.kind === "ready" ? read.record : { version: 1 };
247
+ const proposed = mutate(previous);
248
+ if (!validateRecord(proposed)) {
249
+ return { kind: "invalid", message: "Codex integration record update produced a malformed v1 shape" };
250
+ }
251
+ const record = preserveExtensions(previous, proposed);
252
+ if (!validateRecord(record)) {
253
+ return { kind: "invalid", message: "Codex integration record extension merge produced a malformed v1 shape" };
254
+ }
255
+ const path = recordPath();
256
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
257
+ atomicWriteFile(path, `${JSON.stringify(record, null, 2)}\n`);
258
+ return { kind: "updated", record };
259
+ });
260
+ } catch (error) {
261
+ return {
262
+ kind: "invalid",
263
+ message: `Codex integration record update failed: ${error instanceof Error ? error.message : String(error)}`,
264
+ };
265
+ }
266
+ };
@@ -0,0 +1,203 @@
1
+ import { chmodSync, linkSync, mkdirSync, truncateSync, unlinkSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
3
+
4
+ import {
5
+ AtomicWriteResidualTempError,
6
+ AtomicWriteSecretResidualError,
7
+ atomicWriteFile,
8
+ resolveWriteTarget,
9
+ type AtomicWriteIO,
10
+ } from "../../config";
11
+ import {
12
+ assertCatalogWritePermit,
13
+ type CatalogWritePermit,
14
+ } from "../catalog-write-serialization";
15
+ import {
16
+ forgetEphemeralSecretPath,
17
+ hardenSecretPath,
18
+ } from "../../lib/windows-secret-acl";
19
+
20
+ export interface PreparedCatalogFileWrite {
21
+ readonly path: string;
22
+ readonly content: string;
23
+ }
24
+
25
+ export type CatalogBackupPublication = "written" | "preserved";
26
+
27
+ export interface CatalogBackupWriteIO {
28
+ readonly resolveTarget: (path: string) => string;
29
+ readonly write: (path: string, content: string) => void;
30
+ readonly harden: (path: string) => void;
31
+ /** Must fail with EEXIST rather than replacing an existing destination. */
32
+ readonly publishNoReplace: (source: string, destination: string) => void;
33
+ readonly truncate: (path: string) => void;
34
+ readonly unlink: (path: string) => void;
35
+ }
36
+
37
+ let backupTempSequence = 0;
38
+
39
+ function isMissingPathError(error: unknown): boolean {
40
+ return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
41
+ }
42
+
43
+ function isExistingPathError(error: unknown): boolean {
44
+ return (error as NodeJS.ErrnoException | undefined)?.code === "EEXIST";
45
+ }
46
+
47
+ function defaultBackupWriteIO(destinationPath: string): CatalogBackupWriteIO {
48
+ return {
49
+ resolveTarget: resolveWriteTarget,
50
+ write: (path, content) => writeFileSync(path, content, { encoding: "utf8", mode: 0o600 }),
51
+ harden: path => {
52
+ try { chmodSync(path, 0o600); } catch { /* platform may ignore chmod */ }
53
+ if (process.platform === "win32") {
54
+ hardenSecretPath(path, { required: true, timeoutMemoKey: destinationPath });
55
+ }
56
+ },
57
+ publishNoReplace: linkSync,
58
+ truncate: path => truncateSync(path, 0),
59
+ unlink: unlinkSync,
60
+ };
61
+ }
62
+
63
+ function removePublishedTemp(tempPath: string, io: CatalogBackupWriteIO): void {
64
+ try {
65
+ io.unlink(tempPath);
66
+ forgetEphemeralSecretPath(tempPath);
67
+ return;
68
+ } catch (firstError) {
69
+ if (isMissingPathError(firstError)) {
70
+ forgetEphemeralSecretPath(tempPath);
71
+ return;
72
+ }
73
+ try {
74
+ io.unlink(tempPath);
75
+ forgetEphemeralSecretPath(tempPath);
76
+ return;
77
+ } catch (retryError) {
78
+ if (isMissingPathError(retryError)) {
79
+ forgetEphemeralSecretPath(tempPath);
80
+ return;
81
+ }
82
+ throw new AtomicWriteResidualTempError(tempPath, true, { cause: retryError });
83
+ }
84
+ }
85
+ }
86
+
87
+ function scrubAndRemoveUnpublishedTemp(
88
+ tempPath: string,
89
+ hardened: boolean,
90
+ io: CatalogBackupWriteIO,
91
+ cause: unknown,
92
+ ): void {
93
+ let scrubbed = false;
94
+ try {
95
+ io.truncate(tempPath);
96
+ scrubbed = true;
97
+ } catch (error) {
98
+ if (isMissingPathError(error)) scrubbed = true;
99
+ else {
100
+ try {
101
+ io.write(tempPath, "");
102
+ scrubbed = true;
103
+ } catch { /* removal may still succeed */ }
104
+ }
105
+ }
106
+
107
+ let removed = false;
108
+ try {
109
+ io.unlink(tempPath);
110
+ removed = true;
111
+ } catch (error) {
112
+ if (isMissingPathError(error)) removed = true;
113
+ else {
114
+ try {
115
+ io.unlink(tempPath);
116
+ removed = true;
117
+ } catch (retryError) {
118
+ if (isMissingPathError(retryError)) removed = true;
119
+ }
120
+ }
121
+ }
122
+
123
+ if (!removed && !scrubbed) {
124
+ throw new AtomicWriteSecretResidualError(tempPath, { cause });
125
+ }
126
+ if (!removed && !hardened) {
127
+ try {
128
+ io.harden(tempPath);
129
+ hardened = true;
130
+ } catch { /* zero-byte residual is reported honestly */ }
131
+ }
132
+ if (removed) forgetEphemeralSecretPath(tempPath);
133
+ if (!removed) throw new AtomicWriteResidualTempError(tempPath, hardened, { cause });
134
+ }
135
+
136
+ function publishCatalogBackup(
137
+ prepared: PreparedCatalogFileWrite,
138
+ suppliedIo?: CatalogBackupWriteIO,
139
+ ): CatalogBackupPublication {
140
+ const io = suppliedIo ?? defaultBackupWriteIO(prepared.path);
141
+ const target = io.resolveTarget(prepared.path);
142
+ if (!suppliedIo) mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
143
+ const tempPath = `${target}.ocx.${process.pid}.backup.${++backupTempSequence}.tmp`;
144
+ let hardened = false;
145
+
146
+ try {
147
+ io.write(tempPath, prepared.content);
148
+ io.harden(tempPath);
149
+ hardened = true;
150
+ io.publishNoReplace(tempPath, target);
151
+ } catch (error) {
152
+ scrubAndRemoveUnpublishedTemp(tempPath, hardened, io, error);
153
+ if (isExistingPathError(error)) return "preserved";
154
+ throw error;
155
+ }
156
+
157
+ removePublishedTemp(tempPath, io);
158
+ return "written";
159
+ }
160
+
161
+ /** Replace the active catalog with the caller's already-prepared bytes. */
162
+ export function replaceActiveCodexCatalog(
163
+ permit: CatalogWritePermit,
164
+ owningCodexHome: string,
165
+ prepared: PreparedCatalogFileWrite,
166
+ io?: AtomicWriteIO,
167
+ ): void {
168
+ assertCatalogWritePermit(permit, owningCodexHome);
169
+ atomicWriteFile(prepared.path, prepared.content, io);
170
+ }
171
+
172
+ /** Atomically publish the catalog-path-keyed immutable backup without clobbering. */
173
+ export function publishHashedCodexCatalogBackup(
174
+ permit: CatalogWritePermit,
175
+ owningCodexHome: string,
176
+ prepared: PreparedCatalogFileWrite,
177
+ io?: CatalogBackupWriteIO,
178
+ ): CatalogBackupPublication {
179
+ assertCatalogWritePermit(permit, owningCodexHome);
180
+ return publishCatalogBackup(prepared, io);
181
+ }
182
+
183
+ /** Atomically publish the legacy immutable backup without clobbering. */
184
+ export function publishLegacyCodexCatalogBackup(
185
+ permit: CatalogWritePermit,
186
+ owningCodexHome: string,
187
+ prepared: PreparedCatalogFileWrite,
188
+ io?: CatalogBackupWriteIO,
189
+ ): CatalogBackupPublication {
190
+ assertCatalogWritePermit(permit, owningCodexHome);
191
+ return publishCatalogBackup(prepared, io);
192
+ }
193
+
194
+ /** Replace Codex's models cache with the caller's already-prepared bytes. */
195
+ export function replaceCodexModelsCache(
196
+ permit: CatalogWritePermit,
197
+ owningCodexHome: string,
198
+ prepared: PreparedCatalogFileWrite,
199
+ io?: AtomicWriteIO,
200
+ ): void {
201
+ assertCatalogWritePermit(permit, owningCodexHome);
202
+ atomicWriteFile(prepared.path, prepared.content, io);
203
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * The history mutation boundary. Every byte Codex history owns is written here,
3
+ * and only while H is held.
4
+ *
5
+ * This module exists because the mutations are spread across three surfaces that
6
+ * do not share a transaction: SQLite rows, the backup manifest, and the rollout
7
+ * files. `syncCodexHistoryProvider` writes the manifest BEFORE its database
8
+ * transaction and patches rollouts inside it; restore writes rollouts, then the
9
+ * database, then the manifest, then ejects again
10
+ * (`src/codex/history-provider.ts:606-648,656-698`). A SQLite busy timeout
11
+ * serializes exactly one of those three, which is why an opposite-direction
12
+ * process could overtake through the other two.
13
+ *
14
+ * The permit argument is not decoration and not a type-level claim. Each entry
15
+ * point asks the lock owner at RUNTIME whether the permit it was handed is still
16
+ * live for the state database about to be written, because a permit leaked past
17
+ * its callback type-checks perfectly. A writer reached without H therefore fails
18
+ * closed rather than racing.
19
+ *
20
+ * Reachability: `src/codex/history-worker.ts` is the only permitted production
21
+ * root. Readers and probes stay in `history-provider.ts`; nothing in the CLI, the
22
+ * server, the guardian, `inject.ts` or `sync.ts` may reach these symbols.
23
+ *
24
+ * Design record: devlog/_plan/260804_codex_write_substrate/020_history_isolation.md.
25
+ */
26
+ import { assertHistoryWritePermit, type HistoryWritePermit } from "../history-lock";
27
+ import {
28
+ restoreLegacyOpenaiHistory,
29
+ syncCodexHistoryProvider,
30
+ type CodexHistoryProvider,
31
+ type CodexHistorySyncResult,
32
+ } from "../history-provider";
33
+
34
+ /**
35
+ * Everything a history mutation needs, with no ambient state.
36
+ *
37
+ * The paths are explicit because a Worker is a separate process: it does not
38
+ * inherit the module-load `CODEX_HOME` that `history-provider.ts` resolves at
39
+ * import time (`:16`, `:22`), so a request that relied on those constants would
40
+ * silently address the wrong home.
41
+ */
42
+ export interface HistoryWriteTarget {
43
+ /** Canonical, absolute; the same identity H was acquired for. */
44
+ readonly canonicalStateDbPath: string;
45
+ /** Canonical, absolute path of the backup manifest for that database. */
46
+ readonly canonicalBackupPath: string;
47
+ }
48
+
49
+ /**
50
+ * Apply opencodex routing to resumable history, or restore it to native.
51
+ *
52
+ * `provider` is the DURABLE operation's direction, resolved by the caller from
53
+ * the coordinator row — never a caller-supplied preference. The Worker passes
54
+ * what the row said, which is what stops a request from turning a restore into
55
+ * an apply.
56
+ */
57
+ export function writeHistoryProviderTransition(
58
+ permit: HistoryWritePermit,
59
+ target: HistoryWriteTarget,
60
+ provider: CodexHistoryProvider,
61
+ ): CodexHistorySyncResult {
62
+ assertHistoryWritePermit(permit, target.canonicalStateDbPath);
63
+ return syncCodexHistoryProvider(provider, target.canonicalStateDbPath, target.canonicalBackupPath);
64
+ }
65
+
66
+ /**
67
+ * Manifest-independent legacy ejection.
68
+ *
69
+ * Distinct from the generic restore above: it never reads, consumes, deletes or
70
+ * replaces the backup manifest. It does patch rollout metadata and returns a
71
+ * `files` count, so calling it "DB-only" would be wrong — the name says what it
72
+ * actually avoids.
73
+ */
74
+ export function writeLegacyOpenaiHistoryRecovery(
75
+ permit: HistoryWritePermit,
76
+ target: HistoryWriteTarget,
77
+ ): { rows: number; files: number; failed?: true } {
78
+ assertHistoryWritePermit(permit, target.canonicalStateDbPath);
79
+ return restoreLegacyOpenaiHistory(target.canonicalStateDbPath);
80
+ }
@@ -5,7 +5,16 @@ import { atomicWriteFile } from "../config";
5
5
  import { hasInjectedCodexRouting } from "./injected-marker";
6
6
  import { CODEX_HOME, CODEX_CONFIG_PATH, CODEX_PROFILE_PATH } from "./paths";
7
7
 
8
- const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
8
+ /**
9
+ * Exported so that anything reasoning ABOUT the journal points at the journal.
10
+ *
11
+ * The Codex admission snapshot re-derived this path by hand and got it wrong in
12
+ * both halves — wrong directory and wrong filename — so its "journal identity"
13
+ * watched a file nothing writes. The fixture re-derived it the same wrong way,
14
+ * agreed with the producer, and the pair stayed green. One exported constant
15
+ * removes the opportunity.
16
+ */
17
+ export const JOURNAL_PATH = join(CODEX_HOME, "opencodex-journal.json");
9
18
 
10
19
  interface Journal {
11
20
  version: 1;
@@ -14,6 +14,16 @@ export interface CachedMainAccountInfo extends MainAccountInfo {
14
14
  }
15
15
 
16
16
  let cachedMainAccountInfo: CachedMainAccountInfo | null = null;
17
+ let cachedMainCredentialPresence: boolean | null = null;
18
+ let mainAccountIdentityGeneration = 0;
19
+
20
+ export function captureMainAccountIdentityGeneration(): number {
21
+ return mainAccountIdentityGeneration;
22
+ }
23
+
24
+ export function isMainAccountIdentityGenerationLive(generation: number): boolean {
25
+ return generation === mainAccountIdentityGeneration;
26
+ }
17
27
 
18
28
  export function getMainAccountInfoCache(): CachedMainAccountInfo | null {
19
29
  return cachedMainAccountInfo;
@@ -29,4 +39,18 @@ export function setMainAccountInfoCache(value: CachedMainAccountInfo): void {
29
39
 
30
40
  export function clearMainAccountInfoCache(): void {
31
41
  cachedMainAccountInfo = null;
42
+ mainAccountIdentityGeneration += 1;
43
+ }
44
+
45
+ /** Last physical credential presence observed while native-main ownership was held. */
46
+ export function getMainAccountCredentialPresence(): boolean | null {
47
+ return cachedMainCredentialPresence;
48
+ }
49
+
50
+ export function setMainAccountCredentialPresence(present: boolean): void {
51
+ cachedMainCredentialPresence = present;
52
+ }
53
+
54
+ export function clearMainAccountCredentialPresence(): void {
55
+ cachedMainCredentialPresence = null;
32
56
  }