claude-multiacc 2.0.61 → 2.0.65

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 (244) hide show
  1. package/README.md +6 -2
  2. package/bin/claude-accounts +4 -4
  3. package/bin/codex +54 -13
  4. package/bin/codex-accounts +28 -7
  5. package/docs/ACCOUNT_OPERATIONS.md +2 -2
  6. package/docs/AUTORESUME.md +16 -2
  7. package/docs/CODEX.md +58 -2
  8. package/docs/CODEX_MCP_AUTH.md +58 -0
  9. package/lib/__pycache__/audit.cpython-314.pyc +0 -0
  10. package/lib/__pycache__/autoresume.cpython-314.pyc +0 -0
  11. package/lib/__pycache__/autoresume_cross.cpython-314.pyc +0 -0
  12. package/lib/__pycache__/autoresume_cross_dispatch.cpython-314.pyc +0 -0
  13. package/lib/__pycache__/autoresume_cross_receipt.cpython-314.pyc +0 -0
  14. package/lib/__pycache__/autoresume_handoff.cpython-314.pyc +0 -0
  15. package/lib/__pycache__/autoresume_probe.cpython-314.pyc +0 -0
  16. package/lib/__pycache__/autoresume_receipt.cpython-314.pyc +0 -0
  17. package/lib/__pycache__/claude_client_error.cpython-314.pyc +0 -0
  18. package/lib/__pycache__/claude_reset.cpython-314.pyc +0 -0
  19. package/lib/__pycache__/claude_token_identity.cpython-314.pyc +0 -0
  20. package/lib/__pycache__/codex_autoresume_mode.cpython-314.pyc +0 -0
  21. package/lib/__pycache__/codex_config_edit.cpython-314.pyc +0 -0
  22. package/lib/__pycache__/codex_credit_controls.cpython-314.pyc +0 -0
  23. package/lib/__pycache__/codex_credit_local_scope.cpython-314.pyc +0 -0
  24. package/lib/__pycache__/codex_credit_selection.cpython-314.pyc +0 -0
  25. package/lib/__pycache__/codex_credits.cpython-314.pyc +0 -0
  26. package/lib/__pycache__/codex_limits_lock.cpython-314.pyc +0 -0
  27. package/lib/__pycache__/codex_mcp_oauth.cpython-314.pyc +0 -0
  28. package/lib/__pycache__/codex_python.cpython-314.pyc +0 -0
  29. package/lib/__pycache__/codex_resume_cwd.cpython-314.pyc +0 -0
  30. package/lib/__pycache__/codex_speed.cpython-314.pyc +0 -0
  31. package/lib/__pycache__/codex_trust.cpython-314.pyc +0 -0
  32. package/lib/__pycache__/codex_usage_fetch.cpython-314.pyc +0 -0
  33. package/lib/__pycache__/keychain.cpython-314.pyc +0 -0
  34. package/lib/__pycache__/mcp_mirror.cpython-314.pyc +0 -0
  35. package/lib/__pycache__/mcp_oauth.cpython-314.pyc +0 -0
  36. package/lib/__pycache__/mcp_registry.cpython-314.pyc +0 -0
  37. package/lib/__pycache__/model_catalog.cpython-314.pyc +0 -0
  38. package/lib/__pycache__/model_match.cpython-314.pyc +0 -0
  39. package/lib/__pycache__/report.cpython-314.pyc +0 -0
  40. package/lib/__pycache__/selector_credit_scope.cpython-314.pyc +0 -0
  41. package/lib/__pycache__/selector_credits.cpython-314.pyc +0 -0
  42. package/lib/__pycache__/selector_funding.cpython-314.pyc +0 -0
  43. package/lib/__pycache__/selector_policy.cpython-314.pyc +0 -0
  44. package/lib/__pycache__/selector_primitives.cpython-314.pyc +0 -0
  45. package/lib/__pycache__/session_settings.cpython-314.pyc +0 -0
  46. package/lib/__pycache__/shim_path.cpython-314.pyc +0 -0
  47. package/lib/autoresume.py +34 -13
  48. package/lib/autoresume_cross.py +2 -1
  49. package/lib/autoresume_handoff.py +13 -5
  50. package/lib/codex_exec_stdin.sh +45 -0
  51. package/lib/codex_mcp_oauth.py +204 -0
  52. package/lib/codex_mcp_oauth.sh +29 -0
  53. package/lib/codex_mcp_oauth_locks.py +158 -0
  54. package/lib/codex_mcp_oauth_native.py +102 -0
  55. package/lib/codex_mcp_oauth_store.py +149 -0
  56. package/lib/codex_resume_cwd.py +57 -0
  57. package/lib/codex_retry_process.sh +37 -0
  58. package/lib/codex_speed.py +351 -0
  59. package/lib/codex_speed.sh +39 -0
  60. package/lib/common.sh +16 -3
  61. package/lib/model_match.py +4 -1
  62. package/lib/session_settings.py +25 -6
  63. package/lib/shim_path.py +6 -0
  64. package/package.json +10 -5
  65. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-314.pyc +0 -0
  66. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-314.pyc +0 -0
  67. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-314.pyc +0 -0
  68. package/tests/__pycache__/autoresume_cross_unit_support.cpython-314.pyc +0 -0
  69. package/tests/__pycache__/autoresume_tmux_support.cpython-314.pyc +0 -0
  70. package/tests/__pycache__/packaged_command_support.cpython-314.pyc +0 -0
  71. package/tests/__pycache__/test_autoresume.cpython-314.pyc +0 -0
  72. package/tests/__pycache__/test_autoresume_catalog.cpython-314.pyc +0 -0
  73. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-314.pyc +0 -0
  74. package/tests/__pycache__/test_autoresume_codex_records.cpython-314.pyc +0 -0
  75. package/tests/__pycache__/test_autoresume_codex_trust.cpython-314.pyc +0 -0
  76. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-314.pyc +0 -0
  77. package/tests/__pycache__/test_autoresume_cross_guards.cpython-314.pyc +0 -0
  78. package/tests/__pycache__/test_autoresume_cross_holders.cpython-314.pyc +0 -0
  79. package/tests/__pycache__/test_autoresume_cross_origin.cpython-314.pyc +0 -0
  80. package/tests/__pycache__/test_autoresume_cross_policy.cpython-314.pyc +0 -0
  81. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-314.pyc +0 -0
  82. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-314.pyc +0 -0
  83. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-314.pyc +0 -0
  84. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-314.pyc +0 -0
  85. package/tests/__pycache__/test_autoresume_cursor.cpython-314.pyc +0 -0
  86. package/tests/__pycache__/test_autoresume_cwd.cpython-314.pyc +0 -0
  87. package/tests/__pycache__/test_autoresume_cwd_handoff.cpython-314.pyc +0 -0
  88. package/tests/__pycache__/test_autoresume_cwd_reader.cpython-314.pyc +0 -0
  89. package/tests/__pycache__/test_autoresume_cwd_tmux.cpython-314.pyc +0 -0
  90. package/tests/__pycache__/test_autoresume_fallback.cpython-314.pyc +0 -0
  91. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-314.pyc +0 -0
  92. package/tests/__pycache__/test_autoresume_handoff_history.cpython-314.pyc +0 -0
  93. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-314.pyc +0 -0
  94. package/tests/__pycache__/test_autoresume_memory.cpython-314.pyc +0 -0
  95. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-314.pyc +0 -0
  96. package/tests/__pycache__/test_autoresume_mode.cpython-314.pyc +0 -0
  97. package/tests/__pycache__/test_autoresume_probe.cpython-314.pyc +0 -0
  98. package/tests/__pycache__/test_autoresume_receipt.cpython-314.pyc +0 -0
  99. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-314.pyc +0 -0
  100. package/tests/__pycache__/test_autoresume_settings.cpython-314.pyc +0 -0
  101. package/tests/__pycache__/test_autoresume_sidechain.cpython-314.pyc +0 -0
  102. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-314.pyc +0 -0
  103. package/tests/__pycache__/test_autoresume_tmux.cpython-314.pyc +0 -0
  104. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-314.pyc +0 -0
  105. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-314.pyc +0 -0
  106. package/tests/__pycache__/test_claude_reset.cpython-314.pyc +0 -0
  107. package/tests/__pycache__/test_claude_token_identity.cpython-314.pyc +0 -0
  108. package/tests/__pycache__/test_codex_exec_stdin.cpython-314.pyc +0 -0
  109. package/tests/__pycache__/test_codex_mcp_oauth.cpython-314.pyc +0 -0
  110. package/tests/__pycache__/test_codex_reset.cpython-314.pyc +0 -0
  111. package/tests/__pycache__/test_codex_reset_credits.cpython-314.pyc +0 -0
  112. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-314.pyc +0 -0
  113. package/tests/__pycache__/test_codex_reset_credits_report.cpython-314.pyc +0 -0
  114. package/tests/__pycache__/test_codex_reset_fetch.cpython-314.pyc +0 -0
  115. package/tests/__pycache__/test_codex_reset_polling.cpython-314.pyc +0 -0
  116. package/tests/__pycache__/test_codex_reset_reporting.cpython-314.pyc +0 -0
  117. package/tests/__pycache__/test_codex_reset_windows.cpython-314.pyc +0 -0
  118. package/tests/__pycache__/test_mcp_mirror.cpython-314.pyc +0 -0
  119. package/tests/__pycache__/test_mcp_mirror_args.cpython-314.pyc +0 -0
  120. package/tests/__pycache__/test_mcp_oauth.cpython-314.pyc +0 -0
  121. package/tests/__pycache__/test_mcp_registry.cpython-314.pyc +0 -0
  122. package/tests/__pycache__/test_selector.cpython-314.pyc +0 -0
  123. package/tests/__pycache__/test_selector_cached_credits.cpython-314.pyc +0 -0
  124. package/tests/__pycache__/test_selector_credit_scope.cpython-314.pyc +0 -0
  125. package/tests/__pycache__/test_selector_credit_shim.cpython-314.pyc +0 -0
  126. package/tests/__pycache__/test_selector_credits.cpython-314.pyc +0 -0
  127. package/tests/__pycache__/test_selector_spread.cpython-314.pyc +0 -0
  128. package/tests/__pycache__/test_token_identity_ceremony.cpython-314.pyc +0 -0
  129. package/tests/__pycache__/test_token_identity_integration.cpython-314.pyc +0 -0
  130. package/tests/__pycache__/test_token_identity_retry.cpython-314.pyc +0 -0
  131. package/tests/__pycache__/test_token_identity_sync.cpython-314.pyc +0 -0
  132. package/tests/fake_codex_tui.py +23 -1
  133. package/tests/run-tests.sh +23 -7
  134. package/tests/test_autoresume_cwd.py +71 -0
  135. package/tests/test_autoresume_cwd_handoff.py +92 -0
  136. package/tests/test_autoresume_cwd_reader.py +105 -0
  137. package/tests/test_autoresume_cwd_tmux.py +54 -0
  138. package/tests/test_codex_exec_stdin.py +178 -0
  139. package/tests/test_codex_mcp_oauth.py +146 -0
  140. package/tests/test_codex_mcp_oauth_adopt.py +87 -0
  141. package/tests/test_codex_mcp_oauth_migration.py +141 -0
  142. package/tests/test_codex_mcp_oauth_native.py +109 -0
  143. package/tests/test_codex_mcp_oauth_safety.py +130 -0
  144. package/tests/test_codex_mcp_oauth_shim.py +188 -0
  145. package/tests/test_codex_retry_process.py +143 -0
  146. package/tests/test_codex_speed.py +236 -0
  147. package/tests/test_mutate_lock.py +107 -0
  148. package/tests/test_shim_path.py +44 -4
  149. package/lib/__pycache__/audit.cpython-312.pyc +0 -0
  150. package/lib/__pycache__/autoresume.cpython-312.pyc +0 -0
  151. package/lib/__pycache__/autoresume_cross.cpython-312.pyc +0 -0
  152. package/lib/__pycache__/autoresume_cross_dispatch.cpython-312.pyc +0 -0
  153. package/lib/__pycache__/autoresume_cross_receipt.cpython-312.pyc +0 -0
  154. package/lib/__pycache__/autoresume_handoff.cpython-312.pyc +0 -0
  155. package/lib/__pycache__/autoresume_probe.cpython-312.pyc +0 -0
  156. package/lib/__pycache__/autoresume_receipt.cpython-312.pyc +0 -0
  157. package/lib/__pycache__/claude_client_error.cpython-312.pyc +0 -0
  158. package/lib/__pycache__/claude_reset.cpython-312.pyc +0 -0
  159. package/lib/__pycache__/claude_token_identity.cpython-312.pyc +0 -0
  160. package/lib/__pycache__/codex_autoresume_mode.cpython-312.pyc +0 -0
  161. package/lib/__pycache__/codex_config_edit.cpython-312.pyc +0 -0
  162. package/lib/__pycache__/codex_credit_controls.cpython-312.pyc +0 -0
  163. package/lib/__pycache__/codex_credit_local_scope.cpython-312.pyc +0 -0
  164. package/lib/__pycache__/codex_credit_selection.cpython-312.pyc +0 -0
  165. package/lib/__pycache__/codex_credits.cpython-312.pyc +0 -0
  166. package/lib/__pycache__/codex_limits_lock.cpython-312.pyc +0 -0
  167. package/lib/__pycache__/codex_python.cpython-312.pyc +0 -0
  168. package/lib/__pycache__/codex_trust.cpython-312.pyc +0 -0
  169. package/lib/__pycache__/codex_usage_fetch.cpython-312.pyc +0 -0
  170. package/lib/__pycache__/keychain.cpython-312.pyc +0 -0
  171. package/lib/__pycache__/mcp_mirror.cpython-312.pyc +0 -0
  172. package/lib/__pycache__/mcp_oauth.cpython-312.pyc +0 -0
  173. package/lib/__pycache__/mcp_registry.cpython-312.pyc +0 -0
  174. package/lib/__pycache__/model_catalog.cpython-312.pyc +0 -0
  175. package/lib/__pycache__/model_match.cpython-312.pyc +0 -0
  176. package/lib/__pycache__/report.cpython-312.pyc +0 -0
  177. package/lib/__pycache__/selector_credit_scope.cpython-312.pyc +0 -0
  178. package/lib/__pycache__/selector_credits.cpython-312.pyc +0 -0
  179. package/lib/__pycache__/selector_funding.cpython-312.pyc +0 -0
  180. package/lib/__pycache__/selector_policy.cpython-312.pyc +0 -0
  181. package/lib/__pycache__/selector_primitives.cpython-312.pyc +0 -0
  182. package/lib/__pycache__/session_settings.cpython-312.pyc +0 -0
  183. package/lib/__pycache__/shim_path.cpython-312.pyc +0 -0
  184. package/tests/__pycache__/autoresume_cross_receipt_support.cpython-312.pyc +0 -0
  185. package/tests/__pycache__/autoresume_cross_tmux_environment.cpython-312.pyc +0 -0
  186. package/tests/__pycache__/autoresume_cross_tmux_support.cpython-312.pyc +0 -0
  187. package/tests/__pycache__/autoresume_cross_unit_support.cpython-312.pyc +0 -0
  188. package/tests/__pycache__/autoresume_tmux_support.cpython-312.pyc +0 -0
  189. package/tests/__pycache__/packaged_command_support.cpython-312.pyc +0 -0
  190. package/tests/__pycache__/test_autoresume.cpython-312.pyc +0 -0
  191. package/tests/__pycache__/test_autoresume_catalog.cpython-312.pyc +0 -0
  192. package/tests/__pycache__/test_autoresume_catalog_cli.cpython-312.pyc +0 -0
  193. package/tests/__pycache__/test_autoresume_codex_records.cpython-312.pyc +0 -0
  194. package/tests/__pycache__/test_autoresume_codex_trust.cpython-312.pyc +0 -0
  195. package/tests/__pycache__/test_autoresume_cross_claude_holders.cpython-312.pyc +0 -0
  196. package/tests/__pycache__/test_autoresume_cross_guards.cpython-312.pyc +0 -0
  197. package/tests/__pycache__/test_autoresume_cross_holders.cpython-312.pyc +0 -0
  198. package/tests/__pycache__/test_autoresume_cross_origin.cpython-312.pyc +0 -0
  199. package/tests/__pycache__/test_autoresume_cross_policy.cpython-312.pyc +0 -0
  200. package/tests/__pycache__/test_autoresume_cross_receipt.cpython-312.pyc +0 -0
  201. package/tests/__pycache__/test_autoresume_cross_tmux.cpython-312.pyc +0 -0
  202. package/tests/__pycache__/test_autoresume_cross_tmux_races.cpython-312.pyc +0 -0
  203. package/tests/__pycache__/test_autoresume_cross_tokens.cpython-312.pyc +0 -0
  204. package/tests/__pycache__/test_autoresume_cursor.cpython-312.pyc +0 -0
  205. package/tests/__pycache__/test_autoresume_fallback.cpython-312.pyc +0 -0
  206. package/tests/__pycache__/test_autoresume_handoff_argv.cpython-312.pyc +0 -0
  207. package/tests/__pycache__/test_autoresume_handoff_history.cpython-312.pyc +0 -0
  208. package/tests/__pycache__/test_autoresume_handoff_origins.cpython-312.pyc +0 -0
  209. package/tests/__pycache__/test_autoresume_memory.cpython-312.pyc +0 -0
  210. package/tests/__pycache__/test_autoresume_memory_tmux.cpython-312.pyc +0 -0
  211. package/tests/__pycache__/test_autoresume_mode.cpython-312.pyc +0 -0
  212. package/tests/__pycache__/test_autoresume_probe.cpython-312.pyc +0 -0
  213. package/tests/__pycache__/test_autoresume_receipt.cpython-312.pyc +0 -0
  214. package/tests/__pycache__/test_autoresume_relaunch_settings.cpython-312.pyc +0 -0
  215. package/tests/__pycache__/test_autoresume_settings.cpython-312.pyc +0 -0
  216. package/tests/__pycache__/test_autoresume_sidechain.cpython-312.pyc +0 -0
  217. package/tests/__pycache__/test_autoresume_sidechain_tmux.cpython-312.pyc +0 -0
  218. package/tests/__pycache__/test_autoresume_tmux.cpython-312.pyc +0 -0
  219. package/tests/__pycache__/test_autoresume_tmux_modes.cpython-312.pyc +0 -0
  220. package/tests/__pycache__/test_autoresume_trust_tmux.cpython-312.pyc +0 -0
  221. package/tests/__pycache__/test_claude_reset.cpython-312.pyc +0 -0
  222. package/tests/__pycache__/test_claude_token_identity.cpython-312.pyc +0 -0
  223. package/tests/__pycache__/test_codex_reset.cpython-312.pyc +0 -0
  224. package/tests/__pycache__/test_codex_reset_credits.cpython-312.pyc +0 -0
  225. package/tests/__pycache__/test_codex_reset_credits_polling.cpython-312.pyc +0 -0
  226. package/tests/__pycache__/test_codex_reset_credits_report.cpython-312.pyc +0 -0
  227. package/tests/__pycache__/test_codex_reset_fetch.cpython-312.pyc +0 -0
  228. package/tests/__pycache__/test_codex_reset_polling.cpython-312.pyc +0 -0
  229. package/tests/__pycache__/test_codex_reset_reporting.cpython-312.pyc +0 -0
  230. package/tests/__pycache__/test_codex_reset_windows.cpython-312.pyc +0 -0
  231. package/tests/__pycache__/test_mcp_mirror.cpython-312.pyc +0 -0
  232. package/tests/__pycache__/test_mcp_mirror_args.cpython-312.pyc +0 -0
  233. package/tests/__pycache__/test_mcp_oauth.cpython-312.pyc +0 -0
  234. package/tests/__pycache__/test_mcp_registry.cpython-312.pyc +0 -0
  235. package/tests/__pycache__/test_selector.cpython-312.pyc +0 -0
  236. package/tests/__pycache__/test_selector_cached_credits.cpython-312.pyc +0 -0
  237. package/tests/__pycache__/test_selector_credit_scope.cpython-312.pyc +0 -0
  238. package/tests/__pycache__/test_selector_credit_shim.cpython-312.pyc +0 -0
  239. package/tests/__pycache__/test_selector_credits.cpython-312.pyc +0 -0
  240. package/tests/__pycache__/test_selector_spread.cpython-312.pyc +0 -0
  241. package/tests/__pycache__/test_token_identity_ceremony.cpython-312.pyc +0 -0
  242. package/tests/__pycache__/test_token_identity_integration.cpython-312.pyc +0 -0
  243. package/tests/__pycache__/test_token_identity_retry.cpython-312.pyc +0 -0
  244. package/tests/__pycache__/test_token_identity_sync.cpython-312.pyc +0 -0
@@ -0,0 +1,102 @@
1
+ """Native lock migration primitives; inspect only task-owned lock file metadata."""
2
+ from __future__ import annotations
3
+
4
+ import ctypes
5
+ import os
6
+ from pathlib import Path
7
+ import shutil
8
+ import subprocess
9
+ import sys
10
+
11
+ from codex_mcp_oauth_store import Deferred
12
+
13
+
14
+ def exchange_supported():
15
+ libc = ctypes.CDLL(None, use_errno=True)
16
+ name = 'renamex_np' if sys.platform == 'darwin' else 'renameat2'
17
+ if sys.platform not in ('darwin', 'linux') or not hasattr(libc, name):
18
+ raise Deferred('atomic native lock migration is unavailable')
19
+ if sys.platform == 'darwin' and not shutil.which('lsof'):
20
+ raise Deferred('native lock migration requires lsof')
21
+ if sys.platform == 'linux' and not Path('/proc/self/fd').is_dir():
22
+ raise Deferred('native lock migration requires process file metadata')
23
+
24
+
25
+ def exchange(source, target):
26
+ """Swap a directory and prepared symlink without an absent native pathname."""
27
+ libc = ctypes.CDLL(None, use_errno=True)
28
+ left, right = os.fsencode(source), os.fsencode(target)
29
+ if sys.platform == 'darwin':
30
+ result = libc.renamex_np(ctypes.c_char_p(left), ctypes.c_char_p(right), ctypes.c_uint(2))
31
+ else:
32
+ result = libc.renameat2(-100, ctypes.c_char_p(left), -100, ctypes.c_char_p(right), ctypes.c_uint(2))
33
+ if result:
34
+ raise Deferred('atomic native lock migration failed')
35
+
36
+
37
+ def linux_handles(identities, owned_fds):
38
+ for process in Path('/proc').iterdir():
39
+ if not process.name.isdecimal():
40
+ continue
41
+ try:
42
+ if process.stat().st_uid != os.getuid():
43
+ continue
44
+ for descriptor in (process / 'fd').iterdir():
45
+ if int(process.name) == os.getpid() and int(descriptor.name) in owned_fds:
46
+ continue
47
+ try:
48
+ info = descriptor.stat()
49
+ if (info.st_dev, info.st_ino) in identities:
50
+ return True
51
+ except FileNotFoundError:
52
+ continue
53
+ except FileNotFoundError:
54
+ continue
55
+ except PermissionError as exc:
56
+ if exited_process(process):
57
+ continue
58
+ raise Deferred('native lock handles could not be inspected') from exc
59
+ return False
60
+
61
+
62
+ def exited_process(process):
63
+ """A reaped or single-thread zombie process cannot retain a native lock fd."""
64
+ try:
65
+ fields = dict(line.split(':', 1) for line in (process / 'status').read_text().splitlines()
66
+ if ':' in line)
67
+ except FileNotFoundError:
68
+ return True
69
+ except OSError:
70
+ return False
71
+ # A zombie thread-group leader can still have living threads and open files.
72
+ state = fields.get('State', '').split()
73
+ return bool(state and state[0] in ('Z', 'X') and fields.get('Threads', '').strip() == '1')
74
+
75
+
76
+ def mac_handles(paths, owned_fds):
77
+ try:
78
+ result = subprocess.run(['lsof', '-nP', '-Fpf', '--', *map(str, paths)],
79
+ capture_output=True, text=True, timeout=5, check=False)
80
+ except (OSError, subprocess.TimeoutExpired) as exc:
81
+ raise Deferred('native lock handles could not be inspected') from exc
82
+ if result.returncode not in (0, 1) or result.returncode == 1 and result.stderr:
83
+ raise Deferred('native lock handles could not be inspected')
84
+ pid = None
85
+ for line in result.stdout.splitlines():
86
+ if line.startswith('p') and line[1:].isdecimal():
87
+ pid = int(line[1:])
88
+ elif line.startswith('f'):
89
+ number = line[1:].rstrip('ruw')
90
+ if pid != os.getpid() or not number.isdecimal() or int(number) not in owned_fds:
91
+ return True
92
+ return False
93
+
94
+
95
+ def external_handles(paths, owned_fds):
96
+ paths = list(paths)
97
+ if not paths:
98
+ return False
99
+ identities = {(path.stat().st_dev, path.stat().st_ino) for path in paths}
100
+ if sys.platform == 'linux':
101
+ return linux_handles(identities, owned_fds)
102
+ return mac_handles(paths, owned_fds)
@@ -0,0 +1,149 @@
1
+ """Native Codex fallback document validation and shared-inode persistence."""
2
+ from __future__ import annotations
3
+
4
+ import contextlib
5
+ import json
6
+ import os
7
+ import stat
8
+ from pathlib import Path
9
+
10
+ SENTINEL = '__multiacc_shared_store__|0000000000000000'
11
+ PLACEHOLDER = {
12
+ 'server_name': '__multiacc_shared_store__', 'server_url': 'urn:claude-multiacc:shared-mcp-store',
13
+ 'client_id': '', 'access_token': '', 'refresh_token': None, 'expires_at': 0, 'scopes': [],
14
+ }
15
+ MAX_BYTES = 4 << 20
16
+
17
+
18
+ class Deferred(Exception):
19
+ """Leave an unsafe or contended store unchanged; never include credential values."""
20
+
21
+
22
+ def read_json(path):
23
+ try:
24
+ fd = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK)
25
+ except FileNotFoundError:
26
+ return {}
27
+ with os.fdopen(fd, 'r', encoding='utf-8') as stream:
28
+ if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
29
+ raise Deferred('non-regular document')
30
+ raw = stream.read(MAX_BYTES + 1)
31
+ if len(raw) > MAX_BYTES:
32
+ raise Deferred('oversized document')
33
+ doc = json.loads(raw)
34
+ if not isinstance(doc, dict):
35
+ raise Deferred('invalid document')
36
+ return doc
37
+
38
+
39
+ def validate(doc):
40
+ identities = {}
41
+ for key, entry in doc.items():
42
+ if key == SENTINEL:
43
+ if entry != PLACEHOLDER:
44
+ raise Deferred('reserved identity conflict')
45
+ continue
46
+ if not isinstance(entry, dict) or not isinstance(key, str):
47
+ raise Deferred('invalid credential entry')
48
+ for field in ('server_name', 'server_url', 'client_id', 'access_token'):
49
+ if not isinstance(entry.get(field), str):
50
+ raise Deferred('invalid credential fields')
51
+ if 'executor_owned' in entry and type(entry['executor_owned']) is not bool:
52
+ raise Deferred('invalid credential owner')
53
+ name = entry['server_name']
54
+ if entry.get('executor_owned') or name.startswith(('executor:', 'ema-idp:')) or key.startswith('ema-idp:'):
55
+ raise Deferred('scoped credential cannot be shared')
56
+ if name.removeprefix('local:') == PLACEHOLDER['server_name']:
57
+ raise Deferred('reserved identity conflict')
58
+ issuer = entry.get('issuer')
59
+ if issuer is not None and not isinstance(issuer, str):
60
+ raise Deferred('invalid issuer field')
61
+ refresh, expiry = entry.get('refresh_token'), entry.get('expires_at')
62
+ scopes = entry.get('scopes', [])
63
+ if (refresh is not None and not isinstance(refresh, str)
64
+ or expiry is not None and (type(expiry) is not int or expiry < 0)
65
+ or not isinstance(scopes, list) or not all(isinstance(value, str) for value in scopes)):
66
+ raise Deferred('invalid credential fields')
67
+ identity = (name.removeprefix('local:'), entry['server_url'])
68
+ if identity in identities and identities[identity] != (key, entry):
69
+ raise Deferred('duplicate credential identity')
70
+ identities[identity] = (key, entry)
71
+ return doc
72
+
73
+
74
+ def merge(documents):
75
+ result = {}
76
+ grants = [{key: entry for key, entry in doc.items() if key != SENTINEL} for doc in documents]
77
+ populated = [doc for doc in grants if doc]
78
+ if populated and any(doc != populated[0] for doc in populated[1:]):
79
+ raise Deferred('independent credential documents require separate authorization')
80
+ for doc in documents:
81
+ validate(doc)
82
+ for key, entry in doc.items():
83
+ if key in result and result[key] != entry:
84
+ raise Deferred('conflicting credential grants')
85
+ result[key] = entry
86
+ validate(result)
87
+ result[SENTINEL] = dict(PLACEHOLDER)
88
+ return result
89
+
90
+
91
+ def write_shared(path, doc):
92
+ """Native Codex truncates in place too; callers hold its file-store lock."""
93
+ data = json.dumps(doc, separators=(',', ':')).encode()
94
+ fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_NOFOLLOW | os.O_NONBLOCK, 0o600)
95
+ with os.fdopen(fd, 'wb') as stream:
96
+ if not stat.S_ISREG(os.fstat(stream.fileno()).st_mode):
97
+ raise Deferred('non-regular shared document')
98
+ os.fchmod(stream.fileno(), 0o600)
99
+ stream.truncate(0)
100
+ stream.write(data)
101
+ stream.flush()
102
+ os.fsync(stream.fileno())
103
+
104
+
105
+ @contextlib.contextmanager
106
+ def staged_links(source, accounts, locks):
107
+ """Prove every hardlink and symlink can be created before changing native state."""
108
+ pending, scratch_paths = [], []
109
+ try:
110
+ for account in accounts:
111
+ if source.stat().st_dev != account.stat().st_dev:
112
+ raise Deferred('account credentials are on different filesystems')
113
+ target = account / '.credentials.json'
114
+ if not target.exists() or not os.path.samefile(source, target):
115
+ scratch = account / '.codex-mcp-oauth-link'
116
+ if scratch.is_symlink() or scratch.exists() and not os.path.samefile(source, scratch):
117
+ raise Deferred('foreign staged credential link')
118
+ if not scratch.exists():
119
+ os.link(source, scratch)
120
+ scratch_paths.append(scratch)
121
+ pending.append((scratch, target))
122
+ if not (account / 'mcp-oauth-locks').is_symlink():
123
+ scratch = account / '.codex-mcp-oauth-locks-link'
124
+ if scratch.is_symlink():
125
+ if scratch.resolve() != locks:
126
+ raise Deferred('foreign staged native lock link')
127
+ elif scratch.exists():
128
+ raise Deferred('foreign staged native lock link')
129
+ else:
130
+ scratch.symlink_to(locks, target_is_directory=True)
131
+ scratch_paths.append(scratch)
132
+ yield pending
133
+ finally:
134
+ for scratch in scratch_paths:
135
+ scratch.unlink(missing_ok=True)
136
+
137
+
138
+ def commit_links(pending):
139
+ for scratch, target in pending:
140
+ os.replace(scratch, target)
141
+
142
+
143
+ def save_receipt(path, accounts):
144
+ document = {'accounts': sorted(item.name for item in accounts)}
145
+ if read_json(path) == document:
146
+ return
147
+ scratch = path.with_name('managed.pending.json')
148
+ write_shared(scratch, document)
149
+ os.replace(scratch, path)
@@ -0,0 +1,57 @@
1
+ """Recover Codex's saved working directory from its bound main-thread rollout."""
2
+
3
+ import os
4
+
5
+ from session_settings import _record, _reverse_lines
6
+
7
+
8
+ def _directory(value):
9
+ if isinstance(value, str) and os.path.isabs(value) and not any(c in value for c in "\0\n\r"):
10
+ return value
11
+ return None
12
+
13
+
14
+ def _record_cwd(doc, own):
15
+ payload = doc.get("payload") if doc else None
16
+ if not isinstance(payload, dict) or payload.get("thread_id") not in (None, own):
17
+ return None
18
+ if doc.get("type") == "turn_context":
19
+ turn, root = payload.get("turn_id"), payload.get("root_turn_id")
20
+ if turn and root and turn != root:
21
+ return None
22
+ return _directory(payload.get("cwd"))
23
+ if doc.get("type") == "event_msg" and payload.get("type") == "thread_settings_applied":
24
+ settings = payload.get("thread_settings")
25
+ return _directory(settings.get("cwd")) if isinstance(settings, dict) else None
26
+ return None
27
+
28
+
29
+ def codex_resume_cwd(path, sid=None, upto=None):
30
+ """Latest saved main-thread cwd, then its initial cwd; unknown/unreadable gives None.
31
+
32
+ Match the native resume picker: a settings change can update cwd before the next
33
+ turn_context. Read independently of the watcher's quota cursor so chained restarts
34
+ still recover settings written before that cursor. Only the first metadata record
35
+ identifies this thread; a fork can contain its parent's metadata later in the file.
36
+ ``upto`` bounds history to a validated handoff snapshot's byte boundary.
37
+ """
38
+ if not path:
39
+ return None
40
+ try:
41
+ with open(path, "rb") as fh:
42
+ meta = _record(fh.readline(4 << 20))
43
+ payload = meta.get("payload") if meta and meta.get("type") == "session_meta" else None
44
+ if not isinstance(payload, dict) or not isinstance(payload.get("id"), str):
45
+ return None
46
+ own = payload["id"]
47
+ if sid is not None and sid != own:
48
+ return None
49
+ for line in _reverse_lines(path, upto):
50
+ if b'"cwd"' not in line:
51
+ continue
52
+ directory = _record_cwd(_record(line), own)
53
+ if directory is not None:
54
+ return directory
55
+ return _directory(payload.get("cwd"))
56
+ except (OSError, ValueError):
57
+ return None
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env bash
2
+ # Sourced only by the buffered exec retry path. The caller keeps prompt input on fd 3
3
+ # and owns stdout/stderr redirections and its EXIT cleanup; this helper preserves them.
4
+
5
+ codex_retry_signal() {
6
+ codex_retry_pending_signal="$1"
7
+ codex_retry_signal_status="$2"
8
+ # A signal can arrive between installing the traps and assigning the async PID.
9
+ # Remember it; codex_retry_run delivers it as soon as the owned PID is known.
10
+ [ -n "$codex_retry_child_pid" ] || return 0
11
+ trap '' TERM INT HUP
12
+ kill -s "$codex_retry_pending_signal" "$codex_retry_child_pid" 2>/dev/null || true
13
+ wait "$codex_retry_child_pid" 2>/dev/null || true
14
+ codex_retry_child_pid=""
15
+ exit "$codex_retry_signal_status"
16
+ }
17
+
18
+ codex_retry_run() {
19
+ local codex_retry_child_pid="" codex_retry_pending_signal="" codex_retry_signal_status=0
20
+ local codex_retry_previous_traps rc
21
+ codex_retry_previous_traps="$(trap -p TERM INT HUP)"
22
+ trap 'codex_retry_signal TERM 143' TERM
23
+ trap 'codex_retry_signal INT 130' INT
24
+ trap 'codex_retry_signal HUP 129' HUP
25
+ [ -z "$codex_retry_pending_signal" ] || exit "$codex_retry_signal_status"
26
+ "$@" <&3 &
27
+ codex_retry_child_pid=$!
28
+ if [ -n "$codex_retry_pending_signal" ]; then
29
+ codex_retry_signal "$codex_retry_pending_signal" "$codex_retry_signal_status"
30
+ fi
31
+ if wait "$codex_retry_child_pid"; then rc=0; else rc=$?; fi
32
+ codex_retry_child_pid=""
33
+ trap - TERM INT HUP
34
+ # trap -p emits shell-quoted definitions from this process, never fetched input.
35
+ eval "$codex_retry_previous_traps"
36
+ return "$rc"
37
+ }