@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
@@ -5,7 +5,7 @@ import { MAIN_CODEX_ACCOUNT_ID, setMainAccountPlan } from "./main-account";
5
5
  import { clearAccountQuota } from "./quota";
6
6
  import { clearCodexUpstreamHealthForAccount, clearThreadAccountMapForAccount } from "./routing";
7
7
  import { invalidateCodexWebSocketsForAccount } from "./websocket-registry";
8
- import { clearMainAccountInfoCache } from "./main-account-cache";
8
+ import { clearMainAccountCredentialPresence, clearMainAccountInfoCache } from "./main-account-cache";
9
9
  import { forgetCodexAccountPause } from "./account-pause";
10
10
  import type { OcxConfig } from "../types";
11
11
 
@@ -16,7 +16,16 @@ export function purgeCodexAccountRuntimeState(accountId: string): void {
16
16
  clearAccountQuota(accountId);
17
17
  clearThreadAccountMapForAccount(accountId);
18
18
  clearCodexUpstreamHealthForAccount(accountId);
19
- if (accountId === MAIN_CODEX_ACCOUNT_ID) clearMainAccountInfoCache();
19
+ if (accountId === MAIN_CODEX_ACCOUNT_ID) {
20
+ clearMainAccountInfoCache();
21
+ clearMainAccountCredentialPresence();
22
+ }
23
+ }
24
+
25
+ function purgeMainCodexAccountRuntimeState(): void {
26
+ purgeCodexAccountRuntimeState(MAIN_CODEX_ACCOUNT_ID);
27
+ setMainAccountPlan(null);
28
+ invalidateCodexWebSocketsForAccount(MAIN_CODEX_ACCOUNT_ID);
20
29
  }
21
30
 
22
31
  /**
@@ -34,14 +43,30 @@ export function reconcileMainCodexAccountRuntimeState(): boolean {
34
43
  observedMainChatgptAccountId = currentAccountId;
35
44
  if (previousAccountId === undefined || previousAccountId === currentAccountId) return false;
36
45
 
37
- purgeCodexAccountRuntimeState(MAIN_CODEX_ACCOUNT_ID);
38
- setMainAccountPlan(null);
39
- invalidateCodexWebSocketsForAccount(MAIN_CODEX_ACCOUNT_ID);
46
+ purgeMainCodexAccountRuntimeState();
47
+ return true;
48
+ }
49
+
50
+ /**
51
+ * Apply a transaction-confirmed physical native-login change without waiting for
52
+ * a later auth.json observation. The caller owns credential commit/rollback.
53
+ */
54
+ export function applyConfirmedMainCodexAccountTransition(
55
+ fromAccountId: string,
56
+ toAccountId: string,
57
+ ): boolean {
58
+ if (!fromAccountId || !toAccountId || fromAccountId === toAccountId) {
59
+ if (toAccountId) observedMainChatgptAccountId = toAccountId;
60
+ return false;
61
+ }
62
+ observedMainChatgptAccountId = toAccountId;
63
+ purgeMainCodexAccountRuntimeState();
40
64
  return true;
41
65
  }
42
66
 
43
67
  export function resetMainCodexAccountIdentityTrackingForTests(): void {
44
68
  observedMainChatgptAccountId = undefined;
69
+ clearMainAccountCredentialPresence();
45
70
  }
46
71
 
47
72
  export function deleteCodexAccount(runtimeConfig: OcxConfig, accountId: string): void {
@@ -3,14 +3,34 @@ import { isAccountNeedsReauth } from "./account-runtime-state";
3
3
  import { MAIN_CODEX_ACCOUNT_ID, isMainAccountTokenLive } from "./main-account";
4
4
  import { hasLegacyMainCodexPoolAccount, isSelectableCodexPoolAccount } from "./account-id";
5
5
  import type { OcxConfig } from "../types";
6
+ import { isNativeMainTrafficBlocked } from "./native-profile-startup";
6
7
 
7
- export function isCodexAccountUsable(config: OcxConfig, accountId: string): boolean {
8
+ export interface CodexAccountUsabilityOptions {
9
+ /** Route using cached runtime state only; the caller must reject selected main before auth. */
10
+ nativeMainSelectionOnly?: boolean;
11
+ /** Test seam for proving whether routing attempted a physical native-token read. */
12
+ isMainAccountTokenLive?: typeof isMainAccountTokenLive;
13
+ }
14
+
15
+ export function isCodexAccountUsable(
16
+ config: OcxConfig,
17
+ accountId: string,
18
+ options: CodexAccountUsabilityOptions = {},
19
+ ): boolean {
8
20
  if (accountId === MAIN_CODEX_ACCOUNT_ID) {
21
+ // Startup recovery owns the physical auth/vault boundary. Never parse or select
22
+ // native __main__ while an encrypted switch journal is pending or inconclusive.
23
+ if (!options.nativeMainSelectionOnly && isNativeMainTrafficBlocked()) return false;
9
24
  // A legacy pool row with the sentinel makes an active `__main__` ambiguous.
10
25
  // Fail closed until the authenticated compatibility-delete path removes it.
11
26
  if (hasLegacyMainCodexPoolAccount(config.codexAccounts)) return false;
27
+ if (isAccountNeedsReauth(accountId)) return false;
28
+ // A selection-only caller owns the recovery/drain fence and will reject main
29
+ // before reservation or token materialization. Treat cached main as a routing
30
+ // candidate without touching the credential file so affinity is not rebound.
31
+ if (options.nativeMainSelectionOnly) return true;
12
32
  // Main account: credential is the read-only ~/.codex/auth.json token (Option A).
13
- return isMainAccountTokenLive() && !isAccountNeedsReauth(accountId);
33
+ return (options.isMainAccountTokenLive ?? isMainAccountTokenLive)();
14
34
  }
15
35
  const exists = (config.codexAccounts ?? [])
16
36
  .some(account => isSelectableCodexPoolAccount(account) && account.id === accountId);
@@ -0,0 +1,255 @@
1
+ /**
2
+ * The `AdmissionSnapshot` producer.
3
+ *
4
+ * `AdmissionSnapshot` existed as a TYPE for the whole unit while nothing built
5
+ * one, which meant the write lock's API could only ever be exercised by a
6
+ * fabricated object — the reason WP11 was merged into WP12 rather than shipped
7
+ * as a mechanism with no consumer.
8
+ *
9
+ * What this is FOR: one decision uses one set of bytes. Everything the lock
10
+ * compares is captured here, once, before any artifact is created; the lock then
11
+ * re-reads it while holding N and refuses if the authority moved underneath.
12
+ * Passing the config around instead would let two reads of a changing file
13
+ * disagree inside one operation, which is the interruption hazard this unit
14
+ * exists to close.
15
+ *
16
+ * READS ONLY. Nothing here creates a directory, a database, or a marker: an
17
+ * admission that manufactures the state it is admitting cannot refuse.
18
+ *
19
+ * Design record: devlog/_plan/260804_codex_write_substrate/040_ownership_convergence.md.
20
+ */
21
+ import { createHash } from "node:crypto";
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { join } from "node:path";
24
+
25
+ import { getConfigDir, observeConfigGeneration, readConfigAdmissionSnapshot } from "../config";
26
+ import { inspectNativeCodexOwnership } from "../integrations/native/ownership-preflight";
27
+ import type { AdmissionSnapshot } from "./convergence-types";
28
+ import { codexIntegrationEnabled } from "./desired-state";
29
+ import { externalCodexModelProvider } from "./inject";
30
+ import { JOURNAL_PATH } from "./journal";
31
+ import {
32
+ CODEX_MODELS_CACHE_PATH,
33
+ CODEX_PROFILE_PATH,
34
+ DEFAULT_CATALOG_PATH,
35
+ getCodexHome,
36
+ } from "./paths";
37
+
38
+ export type CodexAdmission =
39
+ | { readonly kind: "admitted"; readonly snapshot: AdmissionSnapshot }
40
+ | {
41
+ readonly kind: "refused";
42
+ readonly authority: "config" | "generation" | "service-home" | "external-provider";
43
+ readonly message: string;
44
+ };
45
+
46
+ /**
47
+ * Seams, not conveniences.
48
+ *
49
+ * The ownership probe shells out to the platform service manager, and a test
50
+ * that reached the real one would be asserting against whatever the developer's
51
+ * machine happens to have installed. Injecting it is also what lets a test
52
+ * observe the EXACT argv the production probe emits, which is the only way to
53
+ * hold down "this never starts or stops anything".
54
+ */
55
+ export interface AdmissionDeps {
56
+ readonly inspectOwnership?: typeof inspectNativeCodexOwnership;
57
+ }
58
+
59
+ /** Hash of a file's exact bytes, or a stable marker for absence. */
60
+ function contentIdentity(path: string): string {
61
+ try {
62
+ return createHash("sha256").update(readFileSync(path)).digest("hex").slice(0, 32);
63
+ } catch {
64
+ // Absence is EVIDENCE, not a hole. An absent journal and an unreadable one
65
+ // are different states and must not collapse to the same identity.
66
+ return existsSync(path) ? "unreadable" : "absent";
67
+ }
68
+ }
69
+
70
+ /**
71
+ * Capture everything one Codex write decision depends on.
72
+ *
73
+ * Refuses rather than guessing: a missing or malformed config, a coordinator
74
+ * that cannot report a generation, a service installed from another home, and an
75
+ * external `model_provider` each end the operation before it starts. The last
76
+ * two are separate authorities on purpose — "someone else's service owns this
77
+ * home" and "the user pointed Codex somewhere else" need different messages
78
+ * because they need different actions.
79
+ */
80
+ export function admitCodexWrite(deps: AdmissionDeps = {}): CodexAdmission {
81
+ const persisted = readConfigAdmissionSnapshot();
82
+ const diagnostics = persisted.diagnostics;
83
+ if (diagnostics.source !== "file") {
84
+ return {
85
+ kind: "refused",
86
+ authority: "config",
87
+ message: diagnostics.source === "default"
88
+ ? "No config file exists to admit a Codex write from."
89
+ : "The config file is malformed; refusing to write from it.",
90
+ };
91
+ }
92
+ if (persisted.kind !== "read") {
93
+ // Unreachable through the union today — `source: "file"` only comes from a
94
+ // successful read — but stated rather than assumed, because the day that
95
+ // stops being true this should refuse, not proceed with no digest.
96
+ return {
97
+ kind: "refused",
98
+ authority: "config",
99
+ message: "The config file could not be read as bytes; refusing to write from it.",
100
+ };
101
+ }
102
+
103
+ /*
104
+ * OBSERVE, not read.
105
+ *
106
+ * `readConfigGeneration` opens the coordinator with `create: true`, so merely
107
+ * admitting would produce a `config-mutation.sqlite` in a home that had none —
108
+ * caught by the "creates nothing" test, which is what that test is for. Only a
109
+ * cooperating config WRITE may create and initialize the singleton.
110
+ */
111
+ const observed = observeConfigGeneration();
112
+ if (observed.kind === "unavailable") {
113
+ return {
114
+ kind: "refused",
115
+ authority: "generation",
116
+ message: observed.reason === "busy"
117
+ ? "The config coordinator is busy; the generation could not be read."
118
+ : "The config generation exists but could not be read; refusing to guess it.",
119
+ };
120
+ }
121
+ const generation = observed.kind === "absent"
122
+ ? { present: false, value: 0 }
123
+ : { present: true, value: observed.generation.value };
124
+
125
+ /*
126
+ * Tri-state, and NOT `assertNativeTeardownOwned` — that one fails open, so a
127
+ * corrupt state file would arrive here as `owned`. Unattended writes refuse on
128
+ * both `foreign` and `unknown`: the first is someone else's home, the second
129
+ * is a question that could not be answered, and neither is permission.
130
+ */
131
+ const ownership = (deps.inspectOwnership ?? inspectNativeCodexOwnership)();
132
+ if (ownership.ownership !== "owned") {
133
+ return {
134
+ kind: "refused",
135
+ authority: "service-home",
136
+ message: ownership.ownership === "foreign"
137
+ ? `Refusing to write: ${ownership.reason}.`
138
+ : `Refusing to write because ownership could not be proven: ${ownership.reason}.`,
139
+ };
140
+ }
141
+
142
+ const codexHome = getCodexHome();
143
+ const codexConfigPath = join(codexHome, "config.toml");
144
+ let external: string | null = null;
145
+ try {
146
+ // Resolved at call time, not at module load: CODEX_CONFIG_PATH is a const
147
+ // fixed when the module was first imported, so it does not follow a
148
+ // CODEX_HOME that changed afterwards.
149
+ external = externalCodexModelProvider(readFileSync(codexConfigPath, "utf-8"));
150
+ } catch {
151
+ // No Codex config yet is not an external owner; it is simply nothing to read.
152
+ external = null;
153
+ }
154
+ if (external) {
155
+ return {
156
+ kind: "refused",
157
+ authority: "external-provider",
158
+ message: `Codex config.toml is owned by an external model_provider (${external}).`,
159
+ };
160
+ }
161
+
162
+ const config = diagnostics.config;
163
+ const opencodexHome = getConfigDir();
164
+ const integrationRecord = join(opencodexHome, "integrations", "codex.json");
165
+ const historyDb = join(codexHome, "state_5.sqlite");
166
+
167
+ const canonicalTargets = {
168
+ codexHome,
169
+ opencodexHome,
170
+ config: codexConfigPath,
171
+ profile: CODEX_PROFILE_PATH,
172
+ catalog: DEFAULT_CATALOG_PATH,
173
+ cache: CODEX_MODELS_CACHE_PATH,
174
+ // The journal's own constant. Re-deriving it here is what made this field
175
+ // watch a path nothing writes.
176
+ journal: JOURNAL_PATH,
177
+ integrationRecord,
178
+ // Backups and rollouts are enumerated by their owners, not guessed here.
179
+ catalogBackups: [] as readonly string[],
180
+ historyDb,
181
+ historyManifest: `${historyDb}.ocx-backup.json`,
182
+ historyRollouts: [] as readonly string[],
183
+ } as const;
184
+
185
+ const snapshot: AdmissionSnapshot = {
186
+ config,
187
+ // The EXACT persisted bytes. Hashing the parsed object instead let a
188
+ // whitespace-only rewrite pass unnoticed, which is precisely the
189
+ // non-cooperating writer this comparison exists to catch.
190
+ configDigest: persisted.contentSha256,
191
+ intent: codexIntegrationEnabled(config) ? "on" : "off",
192
+ generation,
193
+ // Reached only when the projection above said `owned`; the other two states
194
+ // have already refused. This is the observed value, not a placeholder.
195
+ ownership: ownership.ownership,
196
+ externalProvider: null,
197
+ canonicalTargets,
198
+ journalIdentity: contentIdentity(JOURNAL_PATH),
199
+ provenanceIdentity: contentIdentity(integrationRecord),
200
+ authoritySnapshotId: "",
201
+ };
202
+
203
+ return { kind: "admitted", snapshot: { ...snapshot, authoritySnapshotId: hashAuthority(snapshot) } };
204
+ }
205
+
206
+ /**
207
+ * Collapse the generation to one authority token.
208
+ *
209
+ * An absent database and a present zero are the SAME authority — both mean no
210
+ * cooperating write has committed — and they must hash alike, because the lock
211
+ * compares this ID byte-for-byte. They cannot be observed alike: before the lock
212
+ * only absence is visible, and inside it only the present zero is, since the
213
+ * transaction that creates the row has not committed while a separate connection
214
+ * looks. Without this they would never match and every first write would refuse.
215
+ *
216
+ * Canonicalizing inside the hash rather than beside it, in a comparator: a
217
+ * comparator that treats one field specially has to be rewritten at every future
218
+ * comparison site, and the site that gets forgotten is the one that matters.
219
+ */
220
+ function generationAuthority(generation: AdmissionSnapshot["generation"]): string {
221
+ const { present, value } = generation;
222
+ if (!Number.isSafeInteger(value) || value < 0) {
223
+ // NaN, Infinity, fractions, negatives and unsafe integers would otherwise
224
+ // all quietly become "gen:0" — the one value that means "safe to proceed".
225
+ throw new TypeError(`A config generation must be a non-negative safe integer, got ${String(value)}.`);
226
+ }
227
+ return present && value > 0 ? `gen:${value}` : "gen:0";
228
+ }
229
+
230
+ /**
231
+ * Digest every authority field, in a fixed order.
232
+ *
233
+ * The lock compares THIS and nothing else, so a field left out of the hash is a
234
+ * field that can change under the lock without anyone noticing. Adding one to
235
+ * the snapshot means adding it here.
236
+ */
237
+ export function hashAuthority(snapshot: AdmissionSnapshot): string {
238
+ return createHash("sha256")
239
+ .update(JSON.stringify([
240
+ snapshot.configDigest,
241
+ snapshot.intent,
242
+ generationAuthority(snapshot.generation),
243
+ snapshot.ownership,
244
+ snapshot.externalProvider,
245
+ snapshot.canonicalTargets,
246
+ snapshot.journalIdentity,
247
+ snapshot.provenanceIdentity,
248
+ ]))
249
+ .digest("hex");
250
+ }
251
+
252
+ /** Test seam: prove the identity of a path the way admission does. */
253
+ export function admissionContentIdentityForTests(path: string): string {
254
+ return contentIdentity(path);
255
+ }